index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import {
  2. useImperativeHandle,
  3. useLayoutEffect,
  4. forwardRef,
  5. useEffect,
  6. useState,
  7. type Ref,
  8. useRef
  9. } from "react";
  10. import {
  11. TouchableOpacity,
  12. type ViewStyle,
  13. View
  14. } from "react-native";
  15. import type {
  16. IYearSelectorRef,
  17. CalendarYear
  18. } from "./type";
  19. import type IYearSelectorProps from "./type";
  20. import stylesheet, {
  21. useStyles
  22. } from "./stylesheet";
  23. import {
  24. dateSelect
  25. } from "./util";
  26. import {
  27. NCoreUIKitLocalize,
  28. NCoreUIKitTheme,
  29. NCoreUIKitToast
  30. } from "../../core/hooks";
  31. import {
  32. type Mutable
  33. } from "../../types";
  34. import {
  35. ChevronRight as ChevronRightIcon,
  36. ChevronLeft as ChevronLeftIcon
  37. } from "lucide-react-native";
  38. import moment from "moment";
  39. import Text from "../text";
  40. import {
  41. webStyle
  42. } from "../../utils";
  43. const YEAR_LINES = [
  44. [
  45. 0,
  46. 1,
  47. 2
  48. ],
  49. [
  50. 3,
  51. 4,
  52. 5
  53. ],
  54. [
  55. 6,
  56. 7,
  57. 8
  58. ],
  59. [
  60. 9,
  61. 10,
  62. 11
  63. ]
  64. ];
  65. const YearSelector = ({
  66. multipleSelectMinimumRequiredDayCount,
  67. isShowTodayIndicator = true,
  68. viewDate: viewDateProp,
  69. multipleSelectDayLimit,
  70. setIsSheetContentReady,
  71. selectMultipleObject,
  72. selectObject,
  73. dateRange,
  74. maxDate,
  75. minDate,
  76. variant,
  77. date
  78. }: IYearSelectorProps, ref: Ref<IYearSelectorRef>) => {
  79. const {
  80. radiuses,
  81. borders,
  82. spaces,
  83. colors
  84. } = NCoreUIKitTheme.useContext();
  85. const {
  86. localize
  87. } = NCoreUIKitLocalize.useContext();
  88. const selectDate = dateSelect({
  89. dateRange,
  90. variant,
  91. date
  92. });
  93. const [
  94. viewDate,
  95. setViewDate
  96. ] = useState(viewDateProp ? viewDateProp : selectDate);
  97. const [
  98. years,
  99. setYears
  100. ] = useState<Array<CalendarYear>>([]);
  101. const allSelectedYears = useRef<Array<Date>>([]);
  102. const {
  103. nextPrevToolChevronButton: nextPrevToolChevronButtonDynamicStyle,
  104. nextPrevToolContainer: nextPrevToolContainerDynamicStyle,
  105. todayIndicator: todayIndicatorDynamicStyle,
  106. day: dayDynamicStyle
  107. } = useStyles({
  108. radiuses,
  109. borders,
  110. colors,
  111. spaces
  112. });
  113. useEffect(() => {
  114. const allYears = getViewYears({
  115. tDate: viewDate
  116. });
  117. setYears(allYears);
  118. }, [
  119. dateRange,
  120. viewDate,
  121. date
  122. ]);
  123. useLayoutEffect(() => {
  124. if(years && years.length) setIsSheetContentReady(true);
  125. }, [
  126. years
  127. ]);
  128. useEffect(() => {
  129. if(viewDateProp) {
  130. setViewDate(viewDateProp);
  131. }
  132. }, [
  133. viewDateProp
  134. ]);
  135. useImperativeHandle(
  136. ref,
  137. () => ({
  138. changeCurrentView: (date: Date) => {
  139. setViewDate(date);
  140. }
  141. }),
  142. []
  143. );
  144. const getViewYears = ({
  145. tDate
  146. }: {
  147. tDate: Date
  148. }): Array<CalendarYear> => {
  149. allSelectedYears.current = [];
  150. const targetDate = moment(new Date(tDate));
  151. const startOfCalendar = targetDate.clone().subtract(5, "years").startOf("year");
  152. const totalYears = 12;
  153. const allYears = Array.from({
  154. length: totalYears
  155. }).map((_, index) => {
  156. const currentYear = startOfCalendar.clone().add(index, "years");
  157. let isSelected = false;
  158. if(variant === "single" && date) {
  159. allSelectedYears.current = [
  160. date
  161. ];
  162. if(moment(currentYear).isSame(date, "year")) isSelected = true;
  163. } else if(variant === "range" && dateRange && dateRange.start) {
  164. if(dateRange.end) {
  165. allSelectedYears.current = Array.from({
  166. length: moment(dateRange.end).diff(dateRange.start, "years") + 1
  167. }, (_, i) =>
  168. moment(dateRange.start).clone().add(i, "years").toDate()
  169. );
  170. if(moment(currentYear).isBetween(dateRange.start, dateRange.end, "year", "[]")) isSelected = true;
  171. } else if(moment(currentYear).isSame(dateRange.start, "year")) {
  172. allSelectedYears.current = [
  173. dateRange.start
  174. ];
  175. isSelected = true;
  176. }
  177. }
  178. let isDisabled = false;
  179. if(minDate && moment(currentYear).isBefore(moment(minDate).startOf("year"))) {
  180. isDisabled = true;
  181. }
  182. if(maxDate && moment(currentYear).isAfter(moment(maxDate).endOf("year"))) {
  183. isDisabled = true;
  184. }
  185. const currentYearNumber = currentYear.year();
  186. return {
  187. originalIndex: allSelectedYears.current.findIndex((day) => moment(currentYear).isSame(day, "year")),
  188. isToday: currentYear.isSame(moment(), "year"),
  189. title: String(currentYearNumber),
  190. yearNumber: currentYearNumber,
  191. year: currentYearNumber,
  192. date: currentYear,
  193. isDisabled,
  194. isSelected
  195. };
  196. });
  197. return allYears;
  198. };
  199. const renderDay = ({
  200. yearIndex,
  201. yearItem
  202. }: {
  203. yearItem: CalendarYear;
  204. yearIndex: number;
  205. }) => {
  206. const isLastItemSelected = yearItem.isSelected && yearItem.originalIndex === allSelectedYears.current.length - 1;
  207. const isFirstItemSelected = yearItem.isSelected && yearItem.originalIndex === 0;
  208. const selectionStyle: Array<Mutable<ViewStyle> | null> = [
  209. isFirstItemSelected && allSelectedYears.current.length > 1 ? {
  210. borderBottomRightRadius: 0,
  211. borderTopRightRadius: 0
  212. } : null,
  213. isLastItemSelected && allSelectedYears.current.length > 1 ? {
  214. borderBottomLeftRadius: 0,
  215. borderTopLeftRadius: 0
  216. } : null
  217. ];
  218. let dayTitleColor: keyof NCoreUIKit.TextContentColors = "mid";
  219. if(yearItem.isSelected && !isLastItemSelected && !isFirstItemSelected) {
  220. dayTitleColor = "emphasized";
  221. selectionStyle.push({
  222. borderBottomRightRadius: 0,
  223. borderBottomLeftRadius: 0,
  224. borderTopRightRadius: 0,
  225. borderTopLeftRadius: 0
  226. });
  227. } else if(yearItem.isSelected) {
  228. dayTitleColor = "onPrimary";
  229. }
  230. if(yearItem.isDisabled) {
  231. dayTitleColor = "disabled";
  232. }
  233. return <TouchableOpacity
  234. disabled={yearItem.isDisabled}
  235. key={`year-${yearIndex}`}
  236. onPress={() => {
  237. if(variant === "single") {
  238. const newDate = date as Date;
  239. newDate.setFullYear(yearItem.date.toDate().getFullYear());
  240. selectObject(newDate);
  241. } else if(variant === "range") {
  242. if(!dateRange || !dateRange.start) {
  243. selectMultipleObject({
  244. start: new Date(yearItem.date.toDate().setHours(0, 0, 0)),
  245. end: undefined
  246. });
  247. return;
  248. }
  249. if(dateRange && moment(dateRange.start).isSame(yearItem.date, "year")) {
  250. selectMultipleObject({
  251. start: undefined,
  252. end: undefined
  253. });
  254. return;
  255. }
  256. if(dateRange && !dateRange.end) {
  257. if(multipleSelectDayLimit && moment(yearItem.date).diff(dateRange.start, "days") >= multipleSelectDayLimit) {
  258. NCoreUIKitToast.open({
  259. title: localize("maximum-selection-number-of-days-limit-exceeds")
  260. });
  261. return;
  262. }
  263. if(multipleSelectMinimumRequiredDayCount && moment(yearItem.date).diff(dateRange.start, "days") + 1 < multipleSelectMinimumRequiredDayCount) {
  264. NCoreUIKitToast.open({
  265. title: localize("minimum-selection-number-of-days-required-not-provided")
  266. });
  267. return;
  268. }
  269. if(moment(yearItem.date).isBefore(dateRange.start)) {
  270. selectMultipleObject({
  271. start: new Date(yearItem.date.toDate().setHours(0, 0, 0)),
  272. end: undefined
  273. });
  274. } else {
  275. selectMultipleObject({
  276. end: new Date(yearItem.date.toDate().setHours(23, 59, 59)),
  277. start: dateRange?.start
  278. });
  279. }
  280. return;
  281. }
  282. selectMultipleObject({
  283. start: yearItem.date.toDate(),
  284. end: undefined
  285. });
  286. }
  287. }}
  288. style={[
  289. stylesheet.day,
  290. dayDynamicStyle,
  291. yearItem.isSelected ? {
  292. backgroundColor: colors.content.container.primary,
  293. borderColor: colors.content.container.primary
  294. } : null,
  295. yearItem.isSelected && !isLastItemSelected && !isFirstItemSelected ? {
  296. backgroundColor: colors.content.container.emphasized,
  297. borderColor: colors.content.container.emphasized
  298. } : null,
  299. yearItem.isSelected && (yearItem.isDisabled) ? {
  300. opacity: 0.33
  301. } : null,
  302. ...selectionStyle
  303. ]}
  304. >
  305. <Text
  306. variant="labelLargeSize"
  307. color={dayTitleColor}
  308. >
  309. {yearItem.title}
  310. </Text>
  311. {
  312. isShowTodayIndicator && yearItem.isToday ?
  313. <View
  314. style={[
  315. stylesheet.todayIndicator,
  316. selectionStyle,
  317. todayIndicatorDynamicStyle,
  318. yearItem.isSelected ? {
  319. borderColor: colors.content.border.subtle
  320. } : null
  321. ]}
  322. />
  323. :
  324. null
  325. }
  326. </TouchableOpacity>;
  327. };
  328. const renderNextPrevTool = () => {
  329. return <View
  330. style={[
  331. stylesheet.nextPrevToolContainer,
  332. nextPrevToolContainerDynamicStyle
  333. ]}
  334. >
  335. <TouchableOpacity
  336. onPress={() => {
  337. const prevViewDate = moment(viewDate).clone().subtract(12, "years");
  338. setViewDate(prevViewDate.toDate());
  339. }}
  340. style={[
  341. nextPrevToolChevronButtonDynamicStyle
  342. ]}
  343. >
  344. <ChevronLeftIcon
  345. color={colors.content.icon.default}
  346. size={26}
  347. />
  348. </TouchableOpacity>
  349. <Text
  350. variant="labelLargeSize"
  351. color="high"
  352. >
  353. {
  354. years && years.length ?
  355. `${moment(years[0]?.date.toDate()).format("YYYY")} - ${moment(years[years.length - 1]?.date.toDate()).format("YYYY")}`
  356. :
  357. null
  358. }
  359. </Text>
  360. <TouchableOpacity
  361. onPress={() => {
  362. const nextViewDate = moment(viewDate).clone().add(12, "years");
  363. setViewDate(nextViewDate.toDate());
  364. }}
  365. style={[
  366. nextPrevToolChevronButtonDynamicStyle
  367. ]}
  368. >
  369. <ChevronRightIcon
  370. color={colors.content.icon.default}
  371. size={26}
  372. />
  373. </TouchableOpacity>
  374. </View>;
  375. };
  376. return <View
  377. style={[
  378. stylesheet.container
  379. ]}
  380. >
  381. <View>
  382. {renderNextPrevTool()}
  383. </View>
  384. <View
  385. style={[
  386. stylesheet.contentContainer
  387. ]}
  388. >
  389. {years && years.length ? YEAR_LINES.map((yItem, yIndex) => {
  390. const currentYears = yItem.map(yI => years[yI]);
  391. return <View
  392. key={`year-row-${yIndex}`}
  393. style={[
  394. stylesheet.rowContainer,
  395. webStyle({
  396. width: "100%"
  397. })
  398. ]}
  399. >
  400. {currentYears.map((yearItem, yearIndex) => {
  401. return renderDay({
  402. yearItem: yearItem as CalendarYear,
  403. yearIndex
  404. });
  405. })}
  406. </View>;
  407. }) : null}
  408. </View>
  409. </View>;
  410. };
  411. export default forwardRef(YearSelector);