Calender.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import { h } from 'preact';
  2. import { useEffect, useState, useCallback, useMemo, useRef } from 'preact/hooks';
  3. import ArrowRight from '../icons/ArrowRight';
  4. import ArrowRightDouble from '../icons/ArrowRightDouble';
  5. const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf();
  6. const Calender = ({ onChange, calenderRef, close }) => {
  7. const keyRef = useRef([]);
  8. const date = new Date();
  9. const year = date.getFullYear();
  10. const month = date.getMonth();
  11. const daysMap = useMemo(() => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], []);
  12. const monthMap = useMemo(
  13. () => [
  14. 'January',
  15. 'February',
  16. 'March',
  17. 'April',
  18. 'May',
  19. 'June',
  20. 'July',
  21. 'August',
  22. 'September',
  23. 'October',
  24. 'November',
  25. 'December',
  26. ],
  27. []
  28. );
  29. const [state, setState] = useState({
  30. getMonthDetails: [],
  31. year,
  32. month,
  33. selectedDay: null,
  34. timeRange: { before: null, after: null },
  35. monthDetails: null,
  36. });
  37. const getNumberOfDays = useCallback((year, month) => {
  38. return 40 - new Date(year, month, 40).getDate();
  39. }, []);
  40. const getDayDetails = useCallback(
  41. (args) => {
  42. const date = args.index - args.firstDay;
  43. const day = args.index % 7;
  44. let prevMonth = args.month - 1;
  45. let prevYear = args.year;
  46. if (prevMonth < 0) {
  47. prevMonth = 11;
  48. prevYear--;
  49. }
  50. const prevMonthNumberOfDays = getNumberOfDays(prevYear, prevMonth);
  51. const _date = (date < 0 ? prevMonthNumberOfDays + date : date % args.numberOfDays) + 1;
  52. const month = date < 0 ? -1 : date >= args.numberOfDays ? 1 : 0;
  53. const timestamp = new Date(args.year, args.month, _date).getTime();
  54. return {
  55. date: _date,
  56. day,
  57. month,
  58. timestamp,
  59. dayString: daysMap[day],
  60. };
  61. },
  62. [getNumberOfDays, daysMap]
  63. );
  64. const getMonthDetails = useCallback(
  65. (year, month) => {
  66. const firstDay = new Date(year, month).getDay();
  67. const numberOfDays = getNumberOfDays(year, month);
  68. const monthArray = [];
  69. const rows = 6;
  70. let currentDay = null;
  71. let index = 0;
  72. const cols = 7;
  73. for (let row = 0; row < rows; row++) {
  74. for (let col = 0; col < cols; col++) {
  75. currentDay = getDayDetails({
  76. index,
  77. numberOfDays,
  78. firstDay,
  79. year,
  80. month,
  81. });
  82. monthArray.push(currentDay);
  83. index++;
  84. }
  85. }
  86. return monthArray;
  87. },
  88. [getNumberOfDays, getDayDetails]
  89. );
  90. useEffect(() => {
  91. setState((prev) => ({ ...prev, selectedDay: todayTimestamp, monthDetails: getMonthDetails(year, month) }));
  92. }, [year, month, getMonthDetails]);
  93. useEffect(() => {
  94. // add refs for keyboard navigation
  95. if (state.monthDetails) {
  96. keyRef.current = keyRef.current.slice(0, state.monthDetails.length);
  97. }
  98. // set today date in focus for keyboard navigation
  99. const todayDate = new Date(todayTimestamp).getDate();
  100. keyRef.current.find((t) => t.tabIndex === todayDate)?.focus();
  101. }, [state.monthDetails]);
  102. const isCurrentDay = (day) => day.timestamp === todayTimestamp;
  103. const isSelectedRange = useCallback(
  104. (day) => {
  105. if (!state.timeRange.after || !state.timeRange.before) return;
  106. return day.timestamp < state.timeRange.before && day.timestamp >= state.timeRange.after;
  107. },
  108. [state.timeRange]
  109. );
  110. const isFirstDayInRange = useCallback(
  111. (day) => {
  112. if (isCurrentDay(day)) return;
  113. return state.timeRange.after === day.timestamp;
  114. },
  115. [state.timeRange.after]
  116. );
  117. const isLastDayInRange = useCallback(
  118. (day) => {
  119. return state.timeRange.before === new Date(day.timestamp).setHours(24, 0, 0, 0);
  120. },
  121. [state.timeRange.before]
  122. );
  123. const getMonthStr = useCallback(
  124. (month) => {
  125. return monthMap[Math.max(Math.min(11, month), 0)] || 'Month';
  126. },
  127. [monthMap]
  128. );
  129. const onDateClick = (day) => {
  130. const { before, after } = state.timeRange;
  131. let timeRange = { before: null, after: null };
  132. // user has selected a date < after, reset values
  133. if (after === null || day.timestamp < after) {
  134. timeRange = { before: new Date(day.timestamp).setHours(24, 0, 0, 0), after: day.timestamp };
  135. }
  136. // user has selected a date > after
  137. if (after !== null && before !== new Date(day.timestamp).setHours(24, 0, 0, 0) && day.timestamp > after) {
  138. timeRange = {
  139. after,
  140. before:
  141. day.timestamp >= todayTimestamp
  142. ? new Date(todayTimestamp).setHours(24, 0, 0, 0)
  143. : new Date(day.timestamp).setHours(24, 0, 0, 0),
  144. };
  145. }
  146. // reset values
  147. if (before === new Date(day.timestamp).setHours(24, 0, 0, 0)) {
  148. timeRange = { before: null, after: null };
  149. }
  150. setState((prev) => ({
  151. ...prev,
  152. timeRange,
  153. selectedDay: day.timestamp,
  154. }));
  155. if (onChange) {
  156. onChange(timeRange.after ? { before: timeRange.before / 1000, after: timeRange.after / 1000 } : ['all']);
  157. }
  158. };
  159. const setYear = useCallback(
  160. (offset) => {
  161. const year = state.year + offset;
  162. const month = state.month;
  163. setState((prev) => {
  164. return {
  165. ...prev,
  166. year,
  167. monthDetails: getMonthDetails(year, month),
  168. };
  169. });
  170. },
  171. [state.year, state.month, getMonthDetails]
  172. );
  173. const setMonth = (offset) => {
  174. let year = state.year;
  175. let month = state.month + offset;
  176. if (month === -1) {
  177. month = 11;
  178. year--;
  179. } else if (month === 12) {
  180. month = 0;
  181. year++;
  182. }
  183. setState((prev) => {
  184. return {
  185. ...prev,
  186. year,
  187. month,
  188. monthDetails: getMonthDetails(year, month),
  189. };
  190. });
  191. };
  192. const handleKeydown = (e, day, index) => {
  193. if ((keyRef.current && e.key === 'Enter') || e.keyCode === 32) {
  194. e.preventDefault();
  195. day.month === 0 && onDateClick(day);
  196. }
  197. if (e.key === 'ArrowLeft') {
  198. index > 0 && keyRef.current[index - 1].focus();
  199. }
  200. if (e.key === 'ArrowRight') {
  201. index < 41 && keyRef.current[index + 1].focus();
  202. }
  203. if (e.key === 'ArrowUp') {
  204. e.preventDefault();
  205. index > 6 && keyRef.current[index - 7].focus();
  206. }
  207. if (e.key === 'ArrowDown') {
  208. e.preventDefault();
  209. index < 36 && keyRef.current[index + 7].focus();
  210. }
  211. if (e.key === 'Escape') {
  212. close();
  213. }
  214. };
  215. const renderCalendar = () => {
  216. const days =
  217. state.monthDetails &&
  218. state.monthDetails.map((day, idx) => {
  219. return (
  220. <div
  221. onClick={() => onDateClick(day)}
  222. onkeydown={(e) => handleKeydown(e, day, idx)}
  223. ref={(ref) => (keyRef.current[idx] = ref)}
  224. tabIndex={day.month === 0 ? day.date : null}
  225. className={`h-12 w-12 float-left flex flex-shrink justify-center items-center cursor-pointer ${
  226. day.month !== 0 ? ' opacity-50 bg-gray-700 dark:bg-gray-700 pointer-events-none' : ''
  227. }
  228. ${isFirstDayInRange(day) ? ' rounded-l-xl ' : ''}
  229. ${isSelectedRange(day) ? ' bg-blue-600 dark:hover:bg-blue-600' : ''}
  230. ${isLastDayInRange(day) ? ' rounded-r-xl ' : ''}
  231. ${isCurrentDay(day) && !isLastDayInRange(day) ? 'rounded-full bg-gray-100 dark:hover:bg-gray-100 ' : ''}`}
  232. key={idx}
  233. >
  234. <div className="font-light">
  235. <span className="text-gray-400">{day.date}</span>
  236. </div>
  237. </div>
  238. );
  239. });
  240. return (
  241. <div>
  242. <div className="w-full flex justify-start flex-shrink">
  243. {['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map((d, i) => (
  244. <div key={i} className="w-12 text-xs font-light text-center">
  245. {d}
  246. </div>
  247. ))}
  248. </div>
  249. <div className="w-full h-56">{days}</div>
  250. </div>
  251. );
  252. };
  253. return (
  254. <div className="select-none w-96 flex flex-shrink" ref={calenderRef}>
  255. <div className="py-4 px-6">
  256. <div className="flex items-center">
  257. <div className="w-1/6 relative flex justify-around">
  258. <div
  259. tabIndex={100}
  260. className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
  261. onClick={() => setYear(-1)}
  262. >
  263. <ArrowRightDouble className="h-2/6 transform rotate-180 " />
  264. </div>
  265. </div>
  266. <div className="w-1/6 relative flex justify-around ">
  267. <div
  268. tabIndex={101}
  269. className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
  270. onClick={() => setMonth(-1)}
  271. >
  272. <ArrowRight className="h-2/6 transform rotate-180 red" />
  273. </div>
  274. </div>
  275. <div className="w-1/3">
  276. <div className="text-3xl text-center text-gray-200 font-extralight">{state.year}</div>
  277. <div className="text-center text-gray-400 font-extralight">{getMonthStr(state.month)}</div>
  278. </div>
  279. <div className="w-1/6 relative flex justify-around ">
  280. <div
  281. tabIndex={102}
  282. className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
  283. onClick={() => setMonth(1)}
  284. >
  285. <ArrowRight className="h-2/6" />
  286. </div>
  287. </div>
  288. <div className="w-1/6 relative flex justify-around " tabIndex={104} onClick={() => setYear(1)}>
  289. <div className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800">
  290. <ArrowRightDouble className="h-2/6" />
  291. </div>
  292. </div>
  293. </div>
  294. <div className="mt-3">{renderCalendar()}</div>
  295. </div>
  296. </div>
  297. );
  298. };
  299. export default Calender;