feat(calendar): replace CSS-transform zoom with GSAP flying-rect animation and scope data to year
- Add CalendarDateRange input to GraphQL schema; server resolver now accepts from/to params - Frontend query sends year-scoped date range variables reactively - Rewrite zoom-in/zoom-out animations using GSAP flying-rect overlay (650ms vs 2400ms) - Add flying-rect element to CrmCalendarPanel with proper CSS - Remove old calendarSceneTransformStyle CSS-transition approach - Add calendarKillTweens cleanup in onBeforeUnmount Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import gsap from "gsap";
|
||||||
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
||||||
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
|
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
|
||||||
import CrmCalendarPanel from "~~/app/components/workspace/calendar/CrmCalendarPanel.vue";
|
import CrmCalendarPanel from "~~/app/components/workspace/calendar/CrmCalendarPanel.vue";
|
||||||
@@ -596,9 +597,14 @@ const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuer
|
|||||||
{ enabled: apolloAuthReady },
|
{ enabled: apolloAuthReady },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const calendarQueryYear = ref(new Date().getFullYear());
|
||||||
|
const calendarQueryVars = computed(() => ({
|
||||||
|
from: new Date(calendarQueryYear.value, 0, 1).toISOString(),
|
||||||
|
to: new Date(calendarQueryYear.value + 1, 0, 1).toISOString(),
|
||||||
|
}));
|
||||||
const { result: calendarResult, refetch: refetchCalendar } = useQuery(
|
const { result: calendarResult, refetch: refetchCalendar } = useQuery(
|
||||||
CalendarQueryDocument,
|
CalendarQueryDocument,
|
||||||
null,
|
calendarQueryVars,
|
||||||
{ enabled: apolloAuthReady },
|
{ enabled: apolloAuthReady },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2395,6 +2401,7 @@ onBeforeUnmount(() => {
|
|||||||
calendarViewportResizeObserver = null;
|
calendarViewportResizeObserver = null;
|
||||||
}
|
}
|
||||||
clearCalendarZoomPrime();
|
clearCalendarZoomPrime();
|
||||||
|
calendarKillTweens();
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarView = ref<CalendarView>("year");
|
const calendarView = ref<CalendarView>("year");
|
||||||
@@ -2444,7 +2451,10 @@ type CalendarRect = { left: number; top: number; width: number; height: number }
|
|||||||
const calendarContentWrapRef = ref<HTMLElement | null>(null);
|
const calendarContentWrapRef = ref<HTMLElement | null>(null);
|
||||||
const calendarContentScrollRef = ref<HTMLElement | null>(null);
|
const calendarContentScrollRef = ref<HTMLElement | null>(null);
|
||||||
const calendarSceneRef = ref<HTMLElement | null>(null);
|
const calendarSceneRef = ref<HTMLElement | null>(null);
|
||||||
|
const calendarFlyRectRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const calendarFlyVisible = ref(false);
|
||||||
const calendarViewportHeight = ref(0);
|
const calendarViewportHeight = ref(0);
|
||||||
|
let calendarActiveTweens: gsap.core.Tween[] = [];
|
||||||
const calendarHoveredMonthIndex = ref<number | null>(null);
|
const calendarHoveredMonthIndex = ref<number | null>(null);
|
||||||
const calendarHoveredWeekStartKey = ref("");
|
const calendarHoveredWeekStartKey = ref("");
|
||||||
const calendarHoveredDayKey = ref("");
|
const calendarHoveredDayKey = ref("");
|
||||||
@@ -2454,6 +2464,28 @@ function setCalendarContentWrapRef(element: HTMLElement | null) {
|
|||||||
calendarContentWrapRef.value = element;
|
calendarContentWrapRef.value = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCalendarFlyRectRef(element: HTMLDivElement | null) {
|
||||||
|
calendarFlyRectRef.value = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calendarTweenTo(target: gsap.TweenTarget, vars: gsap.TweenVars): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const t = gsap.to(target, {
|
||||||
|
...vars,
|
||||||
|
onComplete: () => {
|
||||||
|
calendarActiveTweens = calendarActiveTweens.filter((tw) => tw !== t);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
calendarActiveTweens.push(t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calendarKillTweens() {
|
||||||
|
for (const t of calendarActiveTweens) t.kill();
|
||||||
|
calendarActiveTweens = [];
|
||||||
|
}
|
||||||
|
|
||||||
function setCalendarContentScrollRef(element: HTMLElement | null) {
|
function setCalendarContentScrollRef(element: HTMLElement | null) {
|
||||||
if (calendarViewportResizeObserver) {
|
if (calendarViewportResizeObserver) {
|
||||||
calendarViewportResizeObserver.disconnect();
|
calendarViewportResizeObserver.disconnect();
|
||||||
@@ -2492,20 +2524,16 @@ function onCalendarSceneMouseLeave() {
|
|||||||
clearCalendarZoomPrime();
|
clearCalendarZoomPrime();
|
||||||
}
|
}
|
||||||
const calendarZoomBusy = ref(false);
|
const calendarZoomBusy = ref(false);
|
||||||
const calendarCameraState = ref({
|
|
||||||
active: false,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
scale: 1,
|
|
||||||
durationMs: 0,
|
|
||||||
});
|
|
||||||
const calendarZoomPrimeToken = ref("");
|
const calendarZoomPrimeToken = ref("");
|
||||||
const calendarZoomPrimeScale = ref(1);
|
const calendarZoomPrimeScale = ref(1);
|
||||||
const calendarZoomPrimeTicks = ref(0);
|
const calendarZoomPrimeTicks = ref(0);
|
||||||
let calendarWheelLockUntil = 0;
|
let calendarWheelLockUntil = 0;
|
||||||
let calendarZoomPrimeTimer: ReturnType<typeof setTimeout> | null = null;
|
let calendarZoomPrimeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let calendarZoomPrimeLastAt = 0;
|
let calendarZoomPrimeLastAt = 0;
|
||||||
const CALENDAR_ZOOM_DURATION_MS = 2400;
|
const CALENDAR_ZOOM_DURATION_MS = 650;
|
||||||
|
const CALENDAR_FLY_DURATION = 0.65;
|
||||||
|
const CALENDAR_FADE_DURATION = 0.18;
|
||||||
|
const CALENDAR_EASE = "power3.inOut";
|
||||||
const CALENDAR_ZOOM_PRIME_STEPS = 2;
|
const CALENDAR_ZOOM_PRIME_STEPS = 2;
|
||||||
const CALENDAR_ZOOM_PRIME_MAX_SCALE = 1.05;
|
const CALENDAR_ZOOM_PRIME_MAX_SCALE = 1.05;
|
||||||
const CALENDAR_ZOOM_PRIME_RESET_MS = 900;
|
const CALENDAR_ZOOM_PRIME_RESET_MS = 900;
|
||||||
@@ -2515,18 +2543,6 @@ const normalizedCalendarView = computed<CalendarHierarchyView>(() =>
|
|||||||
calendarView.value === "agenda" ? "month" : calendarView.value,
|
calendarView.value === "agenda" ? "month" : calendarView.value,
|
||||||
);
|
);
|
||||||
const calendarZoomLevelIndex = computed(() => Math.max(0, calendarZoomOrder.indexOf(normalizedCalendarView.value)));
|
const calendarZoomLevelIndex = computed(() => Math.max(0, calendarZoomOrder.indexOf(normalizedCalendarView.value)));
|
||||||
const calendarSceneTransformStyle = computed(() => {
|
|
||||||
if (!calendarCameraState.value.active) return undefined;
|
|
||||||
return {
|
|
||||||
transform: `translate(${calendarCameraState.value.left}px, ${calendarCameraState.value.top}px) scale(${calendarCameraState.value.scale})`,
|
|
||||||
transformOrigin: "0 0",
|
|
||||||
transition:
|
|
||||||
calendarCameraState.value.durationMs > 0
|
|
||||||
? `transform ${calendarCameraState.value.durationMs}ms cubic-bezier(0.16, 0.86, 0.18, 1)`
|
|
||||||
: "none",
|
|
||||||
willChange: "transform",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function clearCalendarZoomPrime() {
|
function clearCalendarZoomPrime() {
|
||||||
if (calendarZoomPrimeTimer) {
|
if (calendarZoomPrimeTimer) {
|
||||||
@@ -2662,119 +2678,78 @@ function nextAnimationFrame() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForTransformTransition(element: HTMLElement) {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
let settled = false;
|
|
||||||
const finish = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
element.removeEventListener("transitionend", onTransitionEnd);
|
|
||||||
clearTimeout(fallbackTimer);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
const onTransitionEnd = (event: TransitionEvent) => {
|
|
||||||
if (event.target !== element) return;
|
|
||||||
if (event.propertyName !== "transform") return;
|
|
||||||
finish();
|
|
||||||
};
|
|
||||||
const fallbackTimer = setTimeout(() => finish(), CALENDAR_ZOOM_DURATION_MS + 160);
|
|
||||||
element.addEventListener("transitionend", onTransitionEnd);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fadeOutCalendarSiblings(sourceElement: HTMLElement) {
|
|
||||||
const scene = calendarSceneRef.value;
|
|
||||||
if (!scene) return () => {};
|
|
||||||
const targets = Array.from(scene.querySelectorAll<HTMLElement>(".calendar-hover-targetable"));
|
|
||||||
const siblings = targets.filter((element) => {
|
|
||||||
if (element === sourceElement) return false;
|
|
||||||
if (sourceElement.contains(element)) return false;
|
|
||||||
if (element.contains(sourceElement)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const snapshots = siblings.map((element) => ({
|
|
||||||
element,
|
|
||||||
opacity: element.style.opacity,
|
|
||||||
pointerEvents: element.style.pointerEvents,
|
|
||||||
transition: element.style.transition,
|
|
||||||
}));
|
|
||||||
for (const { element } of snapshots) {
|
|
||||||
element.style.transition = "opacity 180ms ease";
|
|
||||||
element.style.opacity = "0";
|
|
||||||
element.style.pointerEvents = "none";
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
for (const snapshot of snapshots) {
|
|
||||||
snapshot.element.style.opacity = snapshot.opacity;
|
|
||||||
snapshot.element.style.pointerEvents = snapshot.pointerEvents;
|
|
||||||
snapshot.element.style.transition = snapshot.transition;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRenderableRect(rect: DOMRect | null) {
|
|
||||||
return Boolean(rect && rect.width >= 2 && rect.height >= 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function animateCalendarFlipTransition(
|
async function animateCalendarFlipTransition(
|
||||||
sourceElement: HTMLElement | null,
|
_sourceElement: HTMLElement | null,
|
||||||
apply: () => void,
|
apply: () => void,
|
||||||
resolveTarget: () => HTMLElement | null,
|
resolveTarget: () => HTMLElement | null,
|
||||||
) {
|
) {
|
||||||
clearCalendarZoomPrime();
|
clearCalendarZoomPrime();
|
||||||
calendarZoomBusy.value = true;
|
calendarZoomBusy.value = true;
|
||||||
let restoreSiblings = () => {};
|
calendarKillTweens();
|
||||||
let animatedElement: HTMLElement | null = null;
|
|
||||||
let snapshot: {
|
const flyEl = calendarFlyRectRef.value;
|
||||||
transform: string;
|
const wrapEl = calendarContentWrapRef.value;
|
||||||
transition: string;
|
const sceneEl = calendarSceneRef.value;
|
||||||
transformOrigin: string;
|
|
||||||
willChange: string;
|
if (!flyEl || !wrapEl) {
|
||||||
zIndex: string;
|
apply();
|
||||||
} | null = null;
|
calendarZoomBusy.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sourceRect = sourceElement?.getBoundingClientRect() ?? null;
|
const wrapRect = wrapEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 1. Fade out current content
|
||||||
|
if (sceneEl) {
|
||||||
|
await calendarTweenTo(sceneEl, { opacity: 0, duration: CALENDAR_FADE_DURATION, ease: "power2.in" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Position fly rect at full viewport
|
||||||
|
const pad = 4;
|
||||||
|
gsap.set(flyEl, {
|
||||||
|
left: pad,
|
||||||
|
top: pad,
|
||||||
|
width: wrapRect.width - pad * 2,
|
||||||
|
height: wrapRect.height - pad * 2,
|
||||||
|
opacity: 1,
|
||||||
|
borderRadius: 14,
|
||||||
|
});
|
||||||
|
calendarFlyVisible.value = true;
|
||||||
|
|
||||||
|
// 3. Switch to parent view
|
||||||
apply();
|
apply();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
// 4. Find target element in new view
|
||||||
const targetElement = resolveTarget();
|
const targetElement = resolveTarget();
|
||||||
const targetRect = targetElement?.getBoundingClientRect() ?? null;
|
const targetRect = targetElement?.getBoundingClientRect() ?? null;
|
||||||
if (!targetElement || !isRenderableRect(sourceRect) || !isRenderableRect(targetRect)) return;
|
|
||||||
|
|
||||||
restoreSiblings = fadeOutCalendarSiblings(targetElement);
|
if (targetElement && targetRect && targetRect.width >= 2 && targetRect.height >= 2) {
|
||||||
animatedElement = targetElement;
|
const tgtLeft = targetRect.left - wrapRect.left;
|
||||||
snapshot = {
|
const tgtTop = targetRect.top - wrapRect.top;
|
||||||
transform: targetElement.style.transform,
|
|
||||||
transition: targetElement.style.transition,
|
|
||||||
transformOrigin: targetElement.style.transformOrigin,
|
|
||||||
willChange: targetElement.style.willChange,
|
|
||||||
zIndex: targetElement.style.zIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dx = sourceRect.left - targetRect.left;
|
// 5. Animate fly rect → target element
|
||||||
const dy = sourceRect.top - targetRect.top;
|
await calendarTweenTo(flyEl, {
|
||||||
const sx = Math.max(0.01, sourceRect.width / targetRect.width);
|
left: tgtLeft,
|
||||||
const sy = Math.max(0.01, sourceRect.height / targetRect.height);
|
top: tgtTop,
|
||||||
|
width: targetRect.width,
|
||||||
targetElement.style.transformOrigin = "top left";
|
height: targetRect.height,
|
||||||
targetElement.style.willChange = "transform";
|
borderRadius: 12,
|
||||||
targetElement.style.zIndex = "24";
|
duration: CALENDAR_FLY_DURATION,
|
||||||
targetElement.style.transition = "none";
|
ease: CALENDAR_EASE,
|
||||||
targetElement.style.transform = `translate3d(${dx}px, ${dy}px, 0px) scale(${sx}, ${sy})`;
|
});
|
||||||
targetElement.getBoundingClientRect();
|
|
||||||
await nextAnimationFrame();
|
|
||||||
|
|
||||||
targetElement.style.transition = `transform ${CALENDAR_ZOOM_DURATION_MS}ms cubic-bezier(0.16, 0.86, 0.18, 1)`;
|
|
||||||
targetElement.style.transform = "translate3d(0px, 0px, 0px) scale(1, 1)";
|
|
||||||
await waitForTransformTransition(targetElement);
|
|
||||||
} finally {
|
|
||||||
if (animatedElement && snapshot) {
|
|
||||||
animatedElement.style.transform = snapshot.transform;
|
|
||||||
animatedElement.style.transition = snapshot.transition;
|
|
||||||
animatedElement.style.transformOrigin = snapshot.transformOrigin;
|
|
||||||
animatedElement.style.willChange = snapshot.willChange;
|
|
||||||
animatedElement.style.zIndex = snapshot.zIndex;
|
|
||||||
}
|
}
|
||||||
restoreSiblings();
|
|
||||||
|
// 6. Hide fly rect, fade in content
|
||||||
|
calendarFlyVisible.value = false;
|
||||||
|
if (sceneEl) {
|
||||||
|
gsap.set(sceneEl, { opacity: 0 });
|
||||||
|
await calendarTweenTo(sceneEl, { opacity: 1, duration: 0.25, ease: "power2.out" });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
calendarFlyVisible.value = false;
|
||||||
calendarZoomBusy.value = false;
|
calendarZoomBusy.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2785,60 +2760,75 @@ async function animateCalendarZoomIntoSource(
|
|||||||
) {
|
) {
|
||||||
clearCalendarZoomPrime();
|
clearCalendarZoomPrime();
|
||||||
calendarZoomBusy.value = true;
|
calendarZoomBusy.value = true;
|
||||||
let restoreSiblings = () => {};
|
calendarKillTweens();
|
||||||
let snapshot: {
|
|
||||||
transform: string;
|
const flyEl = calendarFlyRectRef.value;
|
||||||
transition: string;
|
const wrapEl = calendarContentWrapRef.value;
|
||||||
transformOrigin: string;
|
const scrollEl = calendarContentScrollRef.value;
|
||||||
willChange: string;
|
const sceneEl = calendarSceneRef.value;
|
||||||
zIndex: string;
|
|
||||||
} | null = null;
|
if (!sourceElement || !flyEl || !wrapEl || !scrollEl) {
|
||||||
|
apply();
|
||||||
|
calendarZoomBusy.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const viewportRect = calendarContentScrollRef.value?.getBoundingClientRect() ?? null;
|
const wrapRect = wrapEl.getBoundingClientRect();
|
||||||
const sourceRect = sourceElement?.getBoundingClientRect() ?? null;
|
const sourceRect = sourceElement.getBoundingClientRect();
|
||||||
if (!sourceElement || !isRenderableRect(viewportRect) || !isRenderableRect(sourceRect)) {
|
if (sourceRect.width < 2 || sourceRect.height < 2) {
|
||||||
apply();
|
apply();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreSiblings = fadeOutCalendarSiblings(sourceElement);
|
// 1. Fade out siblings
|
||||||
snapshot = {
|
const siblings = Array.from(
|
||||||
transform: sourceElement.style.transform,
|
sceneEl?.querySelectorAll<HTMLElement>(".calendar-hover-targetable") ?? [],
|
||||||
transition: sourceElement.style.transition,
|
).filter((el) => el !== sourceElement && !sourceElement.contains(el) && !el.contains(sourceElement));
|
||||||
transformOrigin: sourceElement.style.transformOrigin,
|
await calendarTweenTo(siblings, { opacity: 0, duration: CALENDAR_FADE_DURATION, ease: "power2.in" });
|
||||||
willChange: sourceElement.style.willChange,
|
|
||||||
zIndex: sourceElement.style.zIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dx = viewportRect.left - sourceRect.left;
|
// 2. Position fly rect at source element
|
||||||
const dy = viewportRect.top - sourceRect.top;
|
const srcLeft = sourceRect.left - wrapRect.left;
|
||||||
const sx = Math.max(0.01, viewportRect.width / sourceRect.width);
|
const srcTop = sourceRect.top - wrapRect.top;
|
||||||
const sy = Math.max(0.01, viewportRect.height / sourceRect.height);
|
gsap.set(flyEl, {
|
||||||
|
left: srcLeft,
|
||||||
|
top: srcTop,
|
||||||
|
width: sourceRect.width,
|
||||||
|
height: sourceRect.height,
|
||||||
|
opacity: 1,
|
||||||
|
borderRadius: 12,
|
||||||
|
});
|
||||||
|
calendarFlyVisible.value = true;
|
||||||
|
|
||||||
sourceElement.style.transformOrigin = "top left";
|
// 3. Animate fly rect → full viewport
|
||||||
sourceElement.style.willChange = "transform";
|
const pad = 4;
|
||||||
sourceElement.style.zIndex = "24";
|
await calendarTweenTo(flyEl, {
|
||||||
sourceElement.style.transition = "none";
|
left: pad,
|
||||||
sourceElement.style.transform = "translate3d(0px, 0px, 0px) scale(1, 1)";
|
top: pad,
|
||||||
sourceElement.getBoundingClientRect();
|
width: wrapRect.width - pad * 2,
|
||||||
await nextAnimationFrame();
|
height: wrapRect.height - pad * 2,
|
||||||
|
borderRadius: 14,
|
||||||
sourceElement.style.transition = `transform ${CALENDAR_ZOOM_DURATION_MS}ms cubic-bezier(0.16, 0.86, 0.18, 1)`;
|
duration: CALENDAR_FLY_DURATION,
|
||||||
sourceElement.style.transform = `translate3d(${dx}px, ${dy}px, 0px) scale(${sx}, ${sy})`;
|
ease: CALENDAR_EASE,
|
||||||
await waitForTransformTransition(sourceElement);
|
});
|
||||||
|
|
||||||
|
// 4. Switch content
|
||||||
apply();
|
apply();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await nextAnimationFrame();
|
|
||||||
} finally {
|
// 5. Hide fly rect, fade in new content
|
||||||
if (sourceElement && snapshot) {
|
calendarFlyVisible.value = false;
|
||||||
sourceElement.style.transform = snapshot.transform;
|
if (sceneEl) {
|
||||||
sourceElement.style.transition = snapshot.transition;
|
gsap.set(sceneEl, { opacity: 0 });
|
||||||
sourceElement.style.transformOrigin = snapshot.transformOrigin;
|
await calendarTweenTo(sceneEl, { opacity: 1, duration: 0.25, ease: "power2.out" });
|
||||||
sourceElement.style.willChange = snapshot.willChange;
|
|
||||||
sourceElement.style.zIndex = snapshot.zIndex;
|
|
||||||
}
|
}
|
||||||
restoreSiblings();
|
|
||||||
|
// 6. Restore sibling opacity
|
||||||
|
for (const el of siblings) {
|
||||||
|
el.style.opacity = "";
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
calendarFlyVisible.value = false;
|
||||||
calendarZoomBusy.value = false;
|
calendarZoomBusy.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4876,7 +4866,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
:set-calendar-scene-ref="setCalendarSceneRef"
|
:set-calendar-scene-ref="setCalendarSceneRef"
|
||||||
:calendar-viewport-height="calendarViewportHeight"
|
:calendar-viewport-height="calendarViewportHeight"
|
||||||
:normalized-calendar-view="normalizedCalendarView"
|
:normalized-calendar-view="normalizedCalendarView"
|
||||||
:calendar-scene-transform-style="calendarSceneTransformStyle"
|
:calendar-fly-visible="calendarFlyVisible"
|
||||||
|
:set-calendar-fly-rect-ref="setCalendarFlyRectRef"
|
||||||
:on-calendar-scene-mouse-leave="onCalendarSceneMouseLeave"
|
:on-calendar-scene-mouse-leave="onCalendarSceneMouseLeave"
|
||||||
:calendar-view="calendarView"
|
:calendar-view="calendarView"
|
||||||
:year-months="yearMonths"
|
:year-months="yearMonths"
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ defineProps<{
|
|||||||
setCalendarSceneRef: (element: HTMLDivElement | null) => void;
|
setCalendarSceneRef: (element: HTMLDivElement | null) => void;
|
||||||
calendarViewportHeight: number;
|
calendarViewportHeight: number;
|
||||||
normalizedCalendarView: string;
|
normalizedCalendarView: string;
|
||||||
calendarSceneTransformStyle: Record<string, string>;
|
|
||||||
onCalendarSceneMouseLeave: () => void;
|
onCalendarSceneMouseLeave: () => void;
|
||||||
calendarView: string;
|
calendarView: string;
|
||||||
yearMonths: YearMonthItem[];
|
yearMonths: YearMonthItem[];
|
||||||
@@ -80,6 +79,8 @@ defineProps<{
|
|||||||
weekDays: WeekDay[];
|
weekDays: WeekDay[];
|
||||||
calendarPrimeDayToken: (dayKey: string) => string;
|
calendarPrimeDayToken: (dayKey: string) => string;
|
||||||
selectedDayEvents: CalendarEvent[];
|
selectedDayEvents: CalendarEvent[];
|
||||||
|
calendarFlyVisible: boolean;
|
||||||
|
setCalendarFlyRectRef: (element: HTMLDivElement | null) => void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -156,6 +157,14 @@ defineProps<{
|
|||||||
>
|
>
|
||||||
<span>→</span>
|
<span>→</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- GSAP flying rect (zoom transition overlay) -->
|
||||||
|
<div
|
||||||
|
v-show="calendarFlyVisible"
|
||||||
|
:ref="setCalendarFlyRectRef"
|
||||||
|
class="calendar-fly-rect"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:ref="setCalendarContentScrollRef"
|
:ref="setCalendarContentScrollRef"
|
||||||
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
|
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
|
||||||
@@ -167,7 +176,6 @@ defineProps<{
|
|||||||
'calendar-scene',
|
'calendar-scene',
|
||||||
normalizedCalendarView === 'day' ? 'cursor-zoom-out' : 'cursor-zoom-in',
|
normalizedCalendarView === 'day' ? 'cursor-zoom-out' : 'cursor-zoom-in',
|
||||||
]"
|
]"
|
||||||
:style="calendarSceneTransformStyle"
|
|
||||||
@mouseleave="onCalendarSceneMouseLeave"
|
@mouseleave="onCalendarSceneMouseLeave"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -441,6 +449,16 @@ defineProps<{
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-fly-rect {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid color-mix(in oklab, var(--color-primary) 70%, transparent);
|
||||||
|
background: color-mix(in oklab, var(--color-base-200) 60%, transparent);
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
will-change: left, top, width, height;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-zoom-inline {
|
.calendar-zoom-inline {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export type ArchiveCalendarEventInput = {
|
|||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CalendarDateRange = {
|
||||||
|
from?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
to?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CalendarEvent = {
|
export type CalendarEvent = {
|
||||||
__typename?: 'CalendarEvent';
|
__typename?: 'CalendarEvent';
|
||||||
archiveNote: Scalars['String']['output'];
|
archiveNote: Scalars['String']['output'];
|
||||||
@@ -387,6 +392,11 @@ export type Query = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QuerycalendarArgs = {
|
||||||
|
dateRange?: InputMaybe<CalendarDateRange>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QuerygetClientTimelineArgs = {
|
export type QuerygetClientTimelineArgs = {
|
||||||
contactId: Scalars['ID']['input'];
|
contactId: Scalars['ID']['input'];
|
||||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||||
@@ -418,7 +428,10 @@ export type ArchiveChatConversationMutationMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type ArchiveChatConversationMutationMutation = { __typename?: 'Mutation', archiveChatConversation: { __typename?: 'MutationResult', ok: boolean } };
|
export type ArchiveChatConversationMutationMutation = { __typename?: 'Mutation', archiveChatConversation: { __typename?: 'MutationResult', ok: boolean } };
|
||||||
|
|
||||||
export type CalendarQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
export type CalendarQueryQueryVariables = Exact<{
|
||||||
|
from?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
to?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type CalendarQueryQuery = { __typename?: 'Query', calendar: Array<{ __typename?: 'CalendarEvent', id: string, title: string, start: string, end: string, contact: string, note: string, isArchived: boolean, createdAt: string, archiveNote: string, archivedAt: string }> };
|
export type CalendarQueryQuery = { __typename?: 'Query', calendar: Array<{ __typename?: 'CalendarEvent', id: string, title: string, start: string, end: string, contact: string, note: string, isArchived: boolean, createdAt: string, archiveNote: string, archivedAt: string }> };
|
||||||
@@ -670,8 +683,8 @@ export function useArchiveChatConversationMutationMutation(options: VueApolloCom
|
|||||||
}
|
}
|
||||||
export type ArchiveChatConversationMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ArchiveChatConversationMutationMutation, ArchiveChatConversationMutationMutationVariables>;
|
export type ArchiveChatConversationMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ArchiveChatConversationMutationMutation, ArchiveChatConversationMutationMutationVariables>;
|
||||||
export const CalendarQueryDocument = gql`
|
export const CalendarQueryDocument = gql`
|
||||||
query CalendarQuery {
|
query CalendarQuery($from: String, $to: String) {
|
||||||
calendar {
|
calendar(dateRange: {from: $from, to: $to}) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
start
|
start
|
||||||
@@ -693,16 +706,20 @@ export const CalendarQueryDocument = gql`
|
|||||||
* When your component renders, `useCalendarQueryQuery` returns an object from Apollo Client that contains result, loading and error properties
|
* When your component renders, `useCalendarQueryQuery` returns an object from Apollo Client that contains result, loading and error properties
|
||||||
* you can use to render your UI.
|
* you can use to render your UI.
|
||||||
*
|
*
|
||||||
|
* @param variables that will be passed into the query
|
||||||
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
|
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const { result, loading, error } = useCalendarQueryQuery();
|
* const { result, loading, error } = useCalendarQueryQuery({
|
||||||
|
* from: // value for 'from'
|
||||||
|
* to: // value for 'to'
|
||||||
|
* });
|
||||||
*/
|
*/
|
||||||
export function useCalendarQueryQuery(options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
|
export function useCalendarQueryQuery(variables: CalendarQueryQueryVariables | VueCompositionApi.Ref<CalendarQueryQueryVariables> | ReactiveFunction<CalendarQueryQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
|
||||||
return VueApolloComposable.useQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, {}, options);
|
return VueApolloComposable.useQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, variables, options);
|
||||||
}
|
}
|
||||||
export function useCalendarQueryLazyQuery(options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
|
export function useCalendarQueryLazyQuery(variables: CalendarQueryQueryVariables | VueCompositionApi.Ref<CalendarQueryQueryVariables> | ReactiveFunction<CalendarQueryQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CalendarQueryQuery, CalendarQueryQueryVariables>> = {}) {
|
||||||
return VueApolloComposable.useLazyQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, {}, options);
|
return VueApolloComposable.useLazyQuery<CalendarQueryQuery, CalendarQueryQueryVariables>(CalendarQueryDocument, variables, options);
|
||||||
}
|
}
|
||||||
export type CalendarQueryQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<CalendarQueryQuery, CalendarQueryQueryVariables>;
|
export type CalendarQueryQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<CalendarQueryQuery, CalendarQueryQueryVariables>;
|
||||||
export const ChatConversationsQueryDocument = gql`
|
export const ChatConversationsQueryDocument = gql`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query CalendarQuery {
|
query CalendarQuery($from: String, $to: String) {
|
||||||
calendar {
|
calendar(dateRange: { from: $from, to: $to }) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
start
|
start
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ type Query {
|
|||||||
contacts: [Contact!]!
|
contacts: [Contact!]!
|
||||||
communications: [CommItem!]!
|
communications: [CommItem!]!
|
||||||
contactInboxes: [ContactInbox!]!
|
contactInboxes: [ContactInbox!]!
|
||||||
calendar: [CalendarEvent!]!
|
calendar(dateRange: CalendarDateRange): [CalendarEvent!]!
|
||||||
deals: [Deal!]!
|
deals: [Deal!]!
|
||||||
feed: [FeedCard!]!
|
feed: [FeedCard!]!
|
||||||
pins: [CommPin!]!
|
pins: [CommPin!]!
|
||||||
@@ -49,6 +49,11 @@ type PinToggleResult {
|
|||||||
pinned: Boolean!
|
pinned: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CalendarDateRange {
|
||||||
|
from: String
|
||||||
|
to: String
|
||||||
|
}
|
||||||
|
|
||||||
input CreateCalendarEventInput {
|
input CreateCalendarEventInput {
|
||||||
title: String!
|
title: String!
|
||||||
start: String!
|
start: String!
|
||||||
|
|||||||
@@ -675,10 +675,10 @@ async function getContactInboxes(auth: AuthContext | null) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCalendar(auth: AuthContext | null) {
|
async function getCalendar(auth: AuthContext | null, dateRange?: { from?: string; to?: string }) {
|
||||||
const ctx = requireAuth(auth);
|
const ctx = requireAuth(auth);
|
||||||
const from = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
|
const from = dateRange?.from ? new Date(dateRange.from) : new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
|
||||||
const to = new Date(Date.now() + 1000 * 60 * 60 * 24 * 60);
|
const to = dateRange?.to ? new Date(dateRange.to) : new Date(Date.now() + 1000 * 60 * 60 * 24 * 60);
|
||||||
|
|
||||||
const calendarRaw = await prisma.calendarEvent.findMany({
|
const calendarRaw = await prisma.calendarEvent.findMany({
|
||||||
where: { teamId: ctx.teamId, startsAt: { gte: from, lte: to } },
|
where: { teamId: ctx.teamId, startsAt: { gte: from, lte: to } },
|
||||||
@@ -1842,7 +1842,7 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
contacts: [Contact!]!
|
contacts: [Contact!]!
|
||||||
communications: [CommItem!]!
|
communications: [CommItem!]!
|
||||||
contactInboxes: [ContactInbox!]!
|
contactInboxes: [ContactInbox!]!
|
||||||
calendar: [CalendarEvent!]!
|
calendar(dateRange: CalendarDateRange): [CalendarEvent!]!
|
||||||
deals: [Deal!]!
|
deals: [Deal!]!
|
||||||
feed: [FeedCard!]!
|
feed: [FeedCard!]!
|
||||||
pins: [CommPin!]!
|
pins: [CommPin!]!
|
||||||
@@ -1886,6 +1886,11 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
pinned: Boolean!
|
pinned: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CalendarDateRange {
|
||||||
|
from: String
|
||||||
|
to: String
|
||||||
|
}
|
||||||
|
|
||||||
input CreateCalendarEventInput {
|
input CreateCalendarEventInput {
|
||||||
title: String!
|
title: String!
|
||||||
start: String!
|
start: String!
|
||||||
@@ -2113,7 +2118,7 @@ export const crmGraphqlRoot = {
|
|||||||
contacts: async (_args: unknown, context: GraphQLContext) => getContacts(context.auth),
|
contacts: async (_args: unknown, context: GraphQLContext) => getContacts(context.auth),
|
||||||
communications: async (_args: unknown, context: GraphQLContext) => getCommunications(context.auth),
|
communications: async (_args: unknown, context: GraphQLContext) => getCommunications(context.auth),
|
||||||
contactInboxes: async (_args: unknown, context: GraphQLContext) => getContactInboxes(context.auth),
|
contactInboxes: async (_args: unknown, context: GraphQLContext) => getContactInboxes(context.auth),
|
||||||
calendar: async (_args: unknown, context: GraphQLContext) => getCalendar(context.auth),
|
calendar: async (args: { dateRange?: { from?: string; to?: string } }, context: GraphQLContext) => getCalendar(context.auth, args.dateRange ?? undefined),
|
||||||
deals: async (_args: unknown, context: GraphQLContext) => getDeals(context.auth),
|
deals: async (_args: unknown, context: GraphQLContext) => getDeals(context.auth),
|
||||||
feed: async (_args: unknown, context: GraphQLContext) => getFeed(context.auth),
|
feed: async (_args: unknown, context: GraphQLContext) => getFeed(context.auth),
|
||||||
pins: async (_args: unknown, context: GraphQLContext) => getPins(context.auth),
|
pins: async (_args: unknown, context: GraphQLContext) => getPins(context.auth),
|
||||||
|
|||||||
Reference in New Issue
Block a user