daliry-mobile-date-picker 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,14 @@
1
- import { useState, useRef, useEffect } from "react";
2
- import moment, { Moment } from "moment-jalaali";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import type { RefObject, UIEvent } from "react";
3
+ import moment from "moment-jalaali";
4
+ import type { Moment } from "moment";
3
5
  import "./mobileDatePicker.css";
4
- // export * from '.';
5
6
 
6
7
  export interface IDate {
7
- jYear: number;
8
- jMonth: number;
9
- jDay: number;
10
- jDate: string;
8
+ year: number;
9
+ month: number;
10
+ day: number;
11
+ formatted: string;
11
12
  date: string;
12
13
  gDate: string;
13
14
  moment: Moment;
@@ -16,108 +17,159 @@ export interface IDate {
16
17
  interface IMobileDatePicker {
17
18
  onDateChange: (date: IDate) => void;
18
19
  isBirthdate?: boolean;
20
+ isGregorian?: boolean;
21
+ backgroundColor?: string;
22
+ textColor?: string;
23
+ selectedColor?: string;
19
24
  }
20
25
 
21
- const jalaliMonths = [
22
- "", "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
23
- "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", ""
24
- ];
25
-
26
- const years = Array.from({ length: 1308 }, (_, i) => 1308 + i);
27
- const days = Array.from({ length: 33 }, (_, i) => i);
28
-
29
- const MobileDatePicker = ({ onDateChange, isBirthdate = false }: IMobileDatePicker) => {
30
- const today = moment();
31
- const [selectedYear, setSelectedYear] = useState(isBirthdate ? 1388 : today.jYear() - 1);
32
- const [selectedMonth, setSelectedMonth] = useState(isBirthdate ? 6 : today.jMonth());
33
- const [selectedDay, setSelectedDay] = useState(isBirthdate ? 2 : today.jDate() - 1);
34
-
35
- const listRef = useRef<HTMLDivElement>(null);
36
- const listRefMonths = useRef<HTMLDivElement>(null);
37
- const listRefDays = useRef<HTMLDivElement>(null);
38
-
39
- const selectDateFunction = () => {
40
- const gDate = moment(`${selectedYear}-${selectedMonth}-${selectedDay}`, "jYYYY-jMM-jDD").format("YYYY-MM-DD");
41
- onDateChange({
42
- jYear: selectedYear,
43
- jMonth: selectedMonth,
44
- jDay: selectedDay,
45
- jDate: `${selectedYear}-${selectedMonth}-${selectedDay}`,
46
- date: gDate,
47
- gDate,
48
- moment: moment(`${selectedYear}-${selectedMonth}-${selectedDay}`, "jYYYY-jMM-jDD"),
49
- });
50
- };
26
+ const JALALI_MONTHS = ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"];
27
+ const GREGORIAN_MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
51
28
 
52
- useEffect(() => {
53
- if (isBirthdate) selectDateFunction();
54
- }, [selectedYear, selectedMonth, selectedDay]);
29
+ const ITEM_HEIGHT = 45;
30
+
31
+ const DaliryMobileDatePicker = ({
32
+ onDateChange,
33
+ isBirthdate = false,
34
+ isGregorian = false,
35
+ backgroundColor = "#f5f5f5",
36
+ textColor = "#bbb",
37
+ selectedColor = "#333",
38
+ }: IMobileDatePicker) => {
39
+ const now = moment();
40
+
41
+ const initialYear = useMemo(() => {
42
+ const currentYear = isGregorian ? now.year() : now.jYear();
43
+ return isBirthdate ? currentYear - 18 : currentYear;
44
+ }, [isGregorian, isBirthdate]);
45
+
46
+ const initialMonth = isGregorian ? now.month() + 1 : now.jMonth() + 1;
47
+ const initialDay = isGregorian ? now.date() : now.jDate();
48
+
49
+ const [selectedYear, setSelectedYear] = useState<number>(initialYear);
50
+ const [selectedMonth, setSelectedMonth] = useState<number>(initialMonth);
51
+ const [selectedDay, setSelectedDay] = useState<number>(initialDay);
52
+
53
+ const yearRef = useRef<HTMLDivElement | null>(null);
54
+ const monthRef = useRef<HTMLDivElement | null>(null);
55
+ const dayRef = useRef<HTMLDivElement | null>(null);
56
+
57
+ const months = isGregorian ? GREGORIAN_MONTHS : JALALI_MONTHS;
58
+
59
+ const years = useMemo(() => {
60
+ const start = isGregorian ? 1930 : 1300;
61
+ const end = (isGregorian ? now.year() : now.jYear()) + 20;
62
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
63
+ }, [isGregorian, now]);
64
+
65
+ const daysInMonth = useMemo(() => {
66
+ if (isGregorian) {
67
+ return moment(`${selectedYear}-${selectedMonth}-01`, "YYYY-M-DD").daysInMonth();
68
+ }
69
+ return moment.jDaysInMonth(selectedYear, selectedMonth - 1);
70
+ }, [isGregorian, selectedYear, selectedMonth]);
71
+
72
+ const days = useMemo(() => {
73
+ return Array.from({ length: daysInMonth }, (_, i) => i + 1);
74
+ }, [daysInMonth]);
55
75
 
56
76
  useEffect(() => {
57
- listRef.current?.scrollTo({ top: years.indexOf(selectedYear) * 45, behavior: "smooth" });
58
- listRefMonths.current?.scrollTo({ top: selectedMonth * 45, behavior: "smooth" });
59
- listRefDays.current?.scrollTo({ top: selectedDay * 45, behavior: "smooth" });
60
- }, []);
61
-
62
- const handleScroll = () => {
63
- const index = Math.round((listRef.current?.scrollTop || 0) / 45);
64
- setSelectedYear(years[index + 1]);
77
+ if (selectedDay > daysInMonth) {
78
+ setSelectedDay(daysInMonth);
79
+ }
80
+ }, [selectedDay, daysInMonth]);
81
+
82
+ const buildDateObject = (year: number, month: number, day: number): IDate => {
83
+ const m = isGregorian
84
+ ? moment(`${year}-${month}-${day}`, "YYYY-M-D")
85
+ : moment(`${year}-${month}-${day}`, "jYYYY-jM-jD");
86
+
87
+ return {
88
+ year,
89
+ month,
90
+ day,
91
+ formatted: isGregorian ? m.format("YYYY/MM/DD") : m.format("jYYYY/jMM/jDD"),
92
+ date: m.format("YYYY-MM-DD"),
93
+ gDate: m.format("YYYY-MM-DD"),
94
+ moment: m,
95
+ };
65
96
  };
66
97
 
67
- const handleScrollMonths = () => {
68
- const index = Math.round((listRefMonths.current?.scrollTop || 0) / 45);
69
- setSelectedMonth(index + 1);
70
- };
98
+ // پیاده‌سازی Debounce برای ارسال تاریخ
99
+ useEffect(() => {
100
+ const timer = setTimeout(() => {
101
+ onDateChange(buildDateObject(selectedYear, selectedMonth, selectedDay));
102
+ }, 500); // ۵۰۰ میلی‌ثانیه صبر بعد از آخرین تغییر
71
103
 
72
- const handleScrollDays = () => {
73
- const index = Math.round((listRefDays.current?.scrollTop || 0) / 45);
74
- setSelectedDay(index + 1);
75
- };
104
+ return () => clearTimeout(timer);
105
+ }, [selectedYear, selectedMonth, selectedDay, isGregorian]);
76
106
 
77
- return (
78
- <div className="datepicker-container">
79
- <div className="scroll-lists">
80
- <div className="scroll-list" onScroll={handleScrollDays} ref={listRefDays}>
81
- {days.map((day, i) => (
82
- <div
83
- key={i}
84
- className={`scroll-item ${day === selectedDay ? "selected" : ""}`}
85
- >
86
- {day}
87
- </div>
88
- ))}
89
- </div>
107
+ const scrollToIndex = (ref: RefObject<HTMLDivElement | null>, index: number) => {
108
+ if (ref.current) {
109
+ ref.current.scrollTop = index * ITEM_HEIGHT;
110
+ }
111
+ };
90
112
 
91
- <div className="scroll-list" onScroll={handleScrollMonths} ref={listRefMonths}>
92
- {jalaliMonths.map((month, i) => (
93
- <div
94
- key={i}
95
- className={`scroll-item ${i === selectedMonth ? "selected" : ""}`}
96
- >
97
- {month}
98
- </div>
99
- ))}
100
- </div>
113
+ useEffect(() => {
114
+ const monthIndex = Math.max(0, selectedMonth - 1);
115
+ const dayIndex = Math.max(0, selectedDay - 1);
116
+ const yearIndex = Math.max(0, years.indexOf(selectedYear));
117
+
118
+ const timer = window.setTimeout(() => {
119
+ scrollToIndex(dayRef, dayIndex);
120
+ scrollToIndex(monthRef, monthIndex);
121
+ scrollToIndex(yearRef, yearIndex);
122
+ }, 0);
123
+
124
+ return () => window.clearTimeout(timer);
125
+ }, [years]); // فقط وقتی لیست سال‌ها (تقویم) عوض شد ری‌اسکرول کن
126
+
127
+ const handleScroll = (e: UIEvent<HTMLDivElement>, type: "day" | "month" | "year") => {
128
+ const index = Math.round(e.currentTarget.scrollTop / ITEM_HEIGHT);
129
+
130
+ if (type === "day") {
131
+ const nextDay = days[index];
132
+ if (nextDay && nextDay !== selectedDay) setSelectedDay(nextDay);
133
+ } else if (type === "month") {
134
+ const nextMonth = index + 1;
135
+ if (nextMonth >= 1 && nextMonth <= 12 && nextMonth !== selectedMonth) setSelectedMonth(nextMonth);
136
+ } else {
137
+ const nextYear = years[index];
138
+ if (nextYear && nextYear !== selectedYear) setSelectedYear(nextYear);
139
+ }
140
+ };
101
141
 
102
- <div className="scroll-list" onScroll={handleScroll} ref={listRef}>
103
- {years.map((year) => (
142
+ const renderList = (items: Array<string | number>, selectedValue: string | number, ref: RefObject<HTMLDivElement | null>, type: "day" | "month" | "year") => {
143
+ return (
144
+ <div className="scroll-list" onScroll={(e) => handleScroll(e, type)} ref={ref}>
145
+ <div style={{ height: ITEM_HEIGHT }} />
146
+ {items.map((item, index) => {
147
+ const isSelected = type === "month" ? (index + 1) === selectedMonth : item === selectedValue;
148
+ return (
104
149
  <div
105
- key={year}
106
- className={`scroll-item ${year === selectedYear ? "selected" : ""}`}
150
+ key={`${type}-${item}`}
151
+ className={`scroll-item ${isSelected ? "selected" : ""}`}
152
+ style={{ color: isSelected ? selectedColor : textColor }}
107
153
  >
108
- {year}
154
+ {item}
109
155
  </div>
110
- ))}
111
- </div>
156
+ );
157
+ })}
158
+ <div style={{ height: ITEM_HEIGHT }} />
112
159
  </div>
160
+ );
161
+ };
113
162
 
114
- {!isBirthdate && (
115
- <button className="apply-button" onClick={selectDateFunction}>
116
- اعمال تاریخ
117
- </button>
118
- )}
163
+ return (
164
+ <div className="datepicker-container">
165
+ <div className="scroll-lists" style={{ backgroundColor }}>
166
+ {renderList(days, selectedDay, dayRef, "day")}
167
+ {renderList(months, months[selectedMonth - 1], monthRef, "month")}
168
+ {renderList(years, selectedYear, yearRef, "year")}
169
+ <div className="selection-highlight" style={{ top: ITEM_HEIGHT, height: ITEM_HEIGHT }} />
170
+ </div>
119
171
  </div>
120
172
  );
121
173
  };
122
174
 
123
- export default MobileDatePicker;
175
+ export default DaliryMobileDatePicker;
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -2,26 +2,30 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  align-items: center;
5
- gap: 16px;
6
5
  width: 100%;
6
+ min-width: 350px;
7
+ user-select: none;
8
+ padding: 10px;
7
9
  }
8
10
 
9
11
  .scroll-lists {
10
12
  display: flex;
11
13
  justify-content: space-between;
12
- background-color: #f5f5f5;
13
14
  border-radius: 12px;
14
- height: 152px;
15
+ height: 135px; /* 45px * 3 */
15
16
  width: 100%;
17
+ position: relative;
18
+ overflow: hidden;
16
19
  }
17
20
 
18
21
  .scroll-list {
19
- width: 110px;
20
- height: 152px;
22
+ flex: 1;
23
+ height: 135px;
21
24
  overflow-y: auto;
22
25
  scroll-snap-type: y mandatory;
23
26
  scrollbar-width: none;
24
27
  -ms-overflow-style: none;
28
+ z-index: 2;
25
29
  }
26
30
 
27
31
  .scroll-list::-webkit-scrollbar {
@@ -33,27 +37,44 @@
33
37
  line-height: 45px;
34
38
  text-align: center;
35
39
  scroll-snap-align: center;
36
- color: #bbb;
37
- font-size: 16px;
38
- opacity: 0.5;
39
- filter: blur(4px);
40
- font-weight: normal;
40
+ font-size: 19px;
41
+ opacity: 0.4;
42
+ filter: blur(2px);
43
+ transform: scale(0.8);
41
44
  }
42
45
 
43
46
  .scroll-item.selected {
44
- color: #333;
45
- font-size: 20px;
46
47
  font-weight: bold;
47
48
  opacity: 1;
48
49
  filter: none;
50
+ transform: scale(1);
51
+ transition: all 0.1s ease;
52
+ }
53
+
54
+ .selection-highlight {
55
+ position: absolute;
56
+ top: 45px; /* دقیقا ردیف وسط */
57
+ left: 10px;
58
+ right: 10px;
59
+ height: 45px;
60
+ border-top: 1px solid rgba(0,0,0,0.05);
61
+ border-bottom: 1px solid rgba(0,0,0,0.05);
62
+ pointer-events: none;
63
+ z-index: 1;
49
64
  }
50
65
 
51
66
  .apply-button {
52
- padding: 10px 20px;
53
- background-color: #1976d2;
54
- color: white;
67
+ width: 100%;
68
+ margin-top: 16px;
69
+ padding: 12px;
55
70
  border: none;
56
71
  border-radius: 8px;
57
72
  font-size: 16px;
73
+ font-weight: bold;
58
74
  cursor: pointer;
75
+ transition: opacity 0.2s;
76
+ }
77
+
78
+ .apply-button:active {
79
+ opacity: 0.8;
59
80
  }
package/tsconfig.json CHANGED
@@ -1,18 +1,18 @@
1
- {
2
- "compilerOptions": {
3
- "declaration": true,
4
- "emitDeclarationOnly": true,
5
- "outDir": "dist",
6
- "jsx": "react-jsx",
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "module": "ESNext",
10
- "target": "ESNext",
11
- "moduleResolution": "Node",
12
- "strict": true,
13
- "baseUrl": "./src",
14
- "allowSyntheticDefaultImports": true
15
- },
16
- "include": ["src"],
17
- "exclude": ["node_modules", "dist"]
18
- }
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "emitDeclarationOnly": true,
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+ "jsx": "react-jsx",
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "module": "ESNext",
11
+ "target": "ESNext",
12
+ "moduleResolution": "bundler",
13
+ "strict": true,
14
+ "allowSyntheticDefaultImports": true
15
+ },
16
+ "include": ["src"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>