feat: integrate mobile work chat and Gitea delivery

This commit is contained in:
sai karthik
2026-07-26 00:50:11 +05:30
parent 48200a11df
commit 2a0487aa6e
31 changed files with 2477 additions and 220 deletions
+156
View File
@@ -0,0 +1,156 @@
import type {
PointerEvent as ReactPointerEvent,
TransitionEvent as ReactTransitionEvent,
} from "react";
import { useCallback, useEffect, useRef } from "react";
export type SwipeDirection = "left" | "right";
interface UseSwipeCardOptions {
readonly itemKey: string;
readonly onSwipe: (direction: SwipeDirection) => void;
}
const SWIPE_THRESHOLD = 82;
const EXIT_DISTANCE = 460;
const EXIT_DURATION_MS = 180;
export const useSwipeCard = ({ itemKey, onSwipe }: UseSwipeCardOptions) => {
const cardRef = useRef<HTMLElement>(null);
const checkedLabelRef = useRef<HTMLSpanElement>(null);
const backLabelRef = useRef<HTMLSpanElement>(null);
const startXRef = useRef(0);
const distanceRef = useRef(0);
const draggingRef = useRef(false);
const swipingRef = useRef(false);
const pendingDirectionRef = useRef<SwipeDirection | null>(null);
const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const updateVisuals = useCallback((distance: number, animate = false) => {
const card = cardRef.current;
if (!card) {
return;
}
card.style.transition = animate
? `transform ${EXIT_DURATION_MS}ms cubic-bezier(0.16, 1, 0.3, 1)`
: "none";
card.style.transform = `translate3d(${distance}px, 0, 0) rotate(${distance / 32}deg)`;
const actionOpacity = Math.min(Math.abs(distance) / SWIPE_THRESHOLD, 1);
if (checkedLabelRef.current) {
checkedLabelRef.current.style.opacity =
distance > 0 ? String(actionOpacity) : "0";
}
if (backLabelRef.current) {
backLabelRef.current.style.opacity =
distance < 0 ? String(actionOpacity) : "0";
}
}, []);
const reset = useCallback(() => {
if (exitTimerRef.current) {
clearTimeout(exitTimerRef.current);
exitTimerRef.current = null;
}
distanceRef.current = 0;
draggingRef.current = false;
swipingRef.current = false;
pendingDirectionRef.current = null;
updateVisuals(0, true);
}, [updateVisuals]);
const finishSwipe = useCallback(() => {
const direction = pendingDirectionRef.current;
if (!direction) {
return;
}
pendingDirectionRef.current = null;
if (exitTimerRef.current) {
clearTimeout(exitTimerRef.current);
exitTimerRef.current = null;
}
onSwipe(direction);
}, [onSwipe]);
const swipe = useCallback(
(direction: SwipeDirection) => {
if (swipingRef.current) {
return;
}
swipingRef.current = true;
pendingDirectionRef.current = direction;
const distance = direction === "right" ? EXIT_DISTANCE : -EXIT_DISTANCE;
distanceRef.current = distance;
updateVisuals(distance, true);
exitTimerRef.current = setTimeout(finishSwipe, EXIT_DURATION_MS + 80);
},
[finishSwipe, updateVisuals]
);
useEffect(() => {
reset();
return reset;
}, [itemKey, reset]);
const onPointerDown = (event: ReactPointerEvent<HTMLElement>) => {
if (
swipingRef.current ||
(event.pointerType === "mouse" && event.button !== 0)
) {
return;
}
draggingRef.current = true;
startXRef.current = event.clientX;
distanceRef.current = 0;
event.currentTarget.setPointerCapture(event.pointerId);
};
const onPointerMove = (event: ReactPointerEvent<HTMLElement>) => {
if (!draggingRef.current) {
return;
}
distanceRef.current = event.clientX - startXRef.current;
updateVisuals(distanceRef.current);
};
const onPointerUp = (event: ReactPointerEvent<HTMLElement>) => {
if (!draggingRef.current) {
return;
}
draggingRef.current = false;
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
if (Math.abs(distanceRef.current) >= SWIPE_THRESHOLD) {
swipe(distanceRef.current > 0 ? "right" : "left");
return;
}
reset();
};
const onTransitionEnd = (event: ReactTransitionEvent<HTMLElement>) => {
if (
event.currentTarget === event.target &&
event.propertyName === "transform" &&
swipingRef.current
) {
finishSwipe();
}
};
return {
backLabelRef,
cardRef,
checkedLabelRef,
dragged: () => Math.abs(distanceRef.current) > 8,
pointerHandlers: {
onPointerCancel: reset,
onPointerDown,
onPointerMove,
onPointerUp,
},
swipe,
transitionHandlers: {
onTransitionEnd,
},
} as const;
};