219 lines
7.0 KiB
TypeScript
219 lines
7.0 KiB
TypeScript
import { timeFormatter, timeRangeFormatter, TXCalendarEvent } from "@core/app/theme/scheduleView";
|
|
import { useEffect, useState, useRef } from "react";
|
|
import { DaySchedule } from "react-schedule-view";
|
|
import style from "../../theme/scheduleView.module.css";
|
|
import Avatar from "@components/Avatar/Avatar";
|
|
|
|
export interface CalendarRowViewProps {
|
|
daySchedules: DaySchedule<TXCalendarEvent>[];
|
|
handleEventClick: (event: TXCalendarEvent) => void;
|
|
onEventLongPress?: (event: TXCalendarEvent) => void;
|
|
}
|
|
|
|
type CalendarRowViewData = {
|
|
name: string;
|
|
events: TXCalendarEvent[];
|
|
};
|
|
|
|
// Custom component to manage long press state per item
|
|
function EventItem({
|
|
item,
|
|
onClick,
|
|
onLongPress,
|
|
}: {
|
|
item: TXCalendarEvent;
|
|
onClick: () => void;
|
|
onLongPress?: () => void;
|
|
}) {
|
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
const isLongPress = useRef(false);
|
|
const isScrolling = useRef(false);
|
|
const startPos = useRef({ x: 0, y: 0 });
|
|
const [pressed, setPressed] = useState(false);
|
|
|
|
const getAvatar = (data: string | undefined) => {
|
|
let imageAvatar = "";
|
|
if (data) {
|
|
imageAvatar = data;
|
|
}
|
|
return imageAvatar;
|
|
};
|
|
|
|
const startPress = (e: React.TouchEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>) => {
|
|
isLongPress.current = false;
|
|
isScrolling.current = false;
|
|
if ('touches' in e) {
|
|
startPos.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
}
|
|
setPressed(true);
|
|
timerRef.current = setTimeout(() => {
|
|
isLongPress.current = true;
|
|
setPressed(false);
|
|
if (window.navigator && window.navigator.vibrate) {
|
|
window.navigator.vibrate(50); // Haptic feedback
|
|
}
|
|
if (onLongPress) {
|
|
onLongPress();
|
|
}
|
|
}, 500);
|
|
};
|
|
|
|
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
|
|
if (!isScrolling.current) {
|
|
const moveX = e.touches[0].clientX;
|
|
const moveY = e.touches[0].clientY;
|
|
if (
|
|
Math.abs(moveX - startPos.current.x) > 10 ||
|
|
Math.abs(moveY - startPos.current.y) > 10
|
|
) {
|
|
isScrolling.current = true;
|
|
cancelPress();
|
|
}
|
|
}
|
|
};
|
|
|
|
const cancelPress = () => {
|
|
setPressed(false);
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
};
|
|
|
|
const handleClick = () => {
|
|
cancelPress();
|
|
if (!isLongPress.current) {
|
|
onClick();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
transform: pressed ? "scale(0.97)" : "scale(1)",
|
|
transition: "transform 100ms ease",
|
|
|
|
cursor: "pointer",
|
|
backgroundColor: item.color,
|
|
padding: "10px",
|
|
borderRadius: "5px",
|
|
color: "var(--white)",
|
|
fontWeight: "bold",
|
|
marginLeft: "10px",
|
|
marginRight: "10px",
|
|
marginBottom: "5px",
|
|
opacity: item.appointmentId ? 1 : 0.6,
|
|
userSelect: "none",
|
|
WebkitUserSelect: "none",
|
|
}}
|
|
onTouchStart={startPress}
|
|
onTouchEnd={cancelPress}
|
|
onTouchMove={handleTouchMove}
|
|
onTouchCancel={cancelPress}
|
|
onMouseDown={startPress}
|
|
onMouseUp={cancelPress}
|
|
onMouseLeave={cancelPress}
|
|
onClick={handleClick}
|
|
onContextMenu={(e) => e.preventDefault()} // Prevent context menu on long press
|
|
>
|
|
<div>
|
|
<div className={style.turnosXpressTitleContent}>
|
|
<Avatar
|
|
src={getAvatar(item.avatar)}
|
|
alt={item.title}
|
|
size="super-small"
|
|
border="none"
|
|
style={{ minWidth: "32px" }}
|
|
/>
|
|
<h1>{item.title}</h1>
|
|
</div>
|
|
<div className={style.turnosXpressTimeContent}>
|
|
{timeRangeFormatter(item.startTime, item.endTime)}
|
|
{item.payment && (
|
|
<span style={{ marginLeft: "5px", marginTop: "5px" }}>💰</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function CalendarRowView(props: CalendarRowViewProps) {
|
|
const {
|
|
daySchedules = [
|
|
{
|
|
name: "",
|
|
events: [],
|
|
},
|
|
],
|
|
handleEventClick = () => {},
|
|
onEventLongPress,
|
|
} = props;
|
|
|
|
const [data, setData] = useState<CalendarRowViewData[]>([]);
|
|
|
|
const getTimeHeader = (event: TXCalendarEvent): string => {
|
|
return timeFormatter(event.startTime);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const auxData: CalendarRowViewData[] = [];
|
|
for (const item of daySchedules[0].events) {
|
|
const time = getTimeHeader(item);
|
|
const check = auxData.find((x) => x.name === time);
|
|
|
|
if (check) {
|
|
check.events.push(item);
|
|
} else {
|
|
auxData.push({
|
|
name: time,
|
|
events: [item],
|
|
});
|
|
}
|
|
}
|
|
setData(auxData.sort((a, b) => a.name.localeCompare(b.name)));
|
|
}, [daySchedules]);
|
|
|
|
return (
|
|
<>
|
|
{data.map((event: CalendarRowViewData) => {
|
|
return (
|
|
<div
|
|
key={event.name}
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
width: "100%",
|
|
marginLeft: "5px",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontWeight: "bold",
|
|
color: "var(--gray-black)",
|
|
width: "80px",
|
|
}}
|
|
>
|
|
{event.name}
|
|
</div>
|
|
<div style={{ width: "100%" }}>
|
|
{event.events.map((item: TXCalendarEvent) => (
|
|
<EventItem
|
|
key={item.appointmentId || item.repeatId}
|
|
item={item}
|
|
onClick={() => handleEventClick(item)}
|
|
onLongPress={() => {
|
|
if (onEventLongPress && item.appointmentId) {
|
|
onEventLongPress(item);
|
|
}
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
}
|