1+ <script setup>
2+ /** Vendor */
3+ import { DateTime , Info } from " luxon"
4+
5+ /** UI */
6+ import Button from " @/components/ui/Button.vue"
7+ import Popover from " @/components/ui/Popover.vue"
8+
9+ const props = defineProps ({
10+ from: {
11+ type: String ,
12+ default: ' ' ,
13+ },
14+ to: {
15+ type: String ,
16+ default: ' ' ,
17+ },
18+ minDate: {
19+ type: String ,
20+ default: ' ' ,
21+ }
22+ })
23+
24+ const emit = defineEmits ([" onUpdate" ])
25+
26+ const currentDate = ref (DateTime .now ())
27+ const limitMinDate = ref (props .minDate ? DateTime .fromISO (props .minDate ) : ' ' )
28+ const month = ref (currentDate .value .month )
29+ const year = ref (currentDate .value .year )
30+ const startDate = ref (props .from ? DateTime .fromSeconds (parseInt (props .from )) : {})
31+ const endDate = ref (props .to ? DateTime .fromSeconds (parseInt (props .to )) : {})
32+ const weekdays = ref (Info .weekdays (' narrow' , { locale: ' en-US' }))
33+ const days = computed (() => {
34+ let rawDays = []
35+ const firstDay = DateTime .local (year .value , month .value ).setLocale (' en-US' )
36+ const lastDay = firstDay .endOf (' month' )
37+
38+ for (let day = firstDay; day <= lastDay; day = day .plus ({ days: 1 })) {
39+ rawDays .push (day)
40+ }
41+
42+ if (firstDay .weekday !== 1 ) {
43+ let prevDay = firstDay
44+ while (prevDay .weekday !== 1 ) {
45+ prevDay = prevDay .minus ({ days: 1 }).startOf (' day' )
46+ rawDays .unshift (prevDay)
47+ }
48+ }
49+
50+ if (lastDay .weekday !== 7 ) {
51+ let nextDay = lastDay
52+ while (nextDay .weekday !== 7 ) {
53+ nextDay = nextDay .plus ({ days: 1 }).startOf (' day' )
54+ rawDays .push (nextDay)
55+ }
56+ }
57+
58+ let resDays = []
59+ while (rawDays .length ) {
60+ resDays .push (rawDays .splice (0 , 7 ))
61+ }
62+
63+ return resDays
64+ })
65+
66+ const selectedRange = ref (' ' )
67+ const updateSelectedRange = (from , to ) => {
68+ if (from? .ts ) {
69+ if (to? .ts ) {
70+ if (from .year === to .year ) {
71+ selectedRange .value = from .toFormat (' dd LLL' ) !== to .toFormat (' dd LLL' ) ? ` ${ from .toFormat (' dd LLL' )} - ${ to .toFormat (' dd LLL' )} ` : from .toFormat (' dd LLL' )
72+ } else {
73+ selectedRange .value = ` ${ from .toFormat (' dd LLL yyyy' )} - ${ to .toFormat (' dd LLL yyyy' )} `
74+ }
75+ } else {
76+ selectedRange .value = from .toFormat (' dd LLL' )
77+ }
78+ } else {
79+ selectedRange .value = ' '
80+ }
81+ }
82+ updateSelectedRange (startDate .value , endDate .value )
83+
84+ const isNextMonthAvailable = computed (() => ! (month .value === currentDate .value .month && year .value === currentDate .value .year ))
85+ const isPrevMonthAvailable = computed (() => limitMinDate .value ? limitMinDate .value .ts < days .value [0 ][0 ].ts : true )
86+ const isDayAvailable = (d ) => {
87+ if (d .startOf (' day' ).ts > currentDate .value .startOf (' day' ).ts ) {
88+ return false
89+ } else if (limitMinDate .value ) {
90+ return d .startOf (' day' ).ts >= limitMinDate .value .startOf (' day' ).ts
91+ } else {
92+ return true
93+ }
94+ }
95+
96+ const handleSelectDate = (d ) => {
97+ if (! startDate .value .ts ) {
98+ startDate .value = d
99+ } else if (startDate .value .ts !== d .ts ) {
100+ if (! endDate .value .ts ) {
101+ if (startDate .value .ts > d .ts ) {
102+ endDate .value = startDate .value
103+ startDate .value = d
104+ } else {
105+ endDate .value = d
106+ }
107+ } else {
108+ startDate .value = d
109+ endDate .value = {}
110+ }
111+ } else {
112+ if (! endDate .value .ts ) {
113+ startDate .value = {}
114+ } else {
115+ startDate .value = endDate .value
116+ endDate .value = {}
117+ }
118+ }
119+ }
120+
121+ const isInSelectedPeriod = (d ) => {
122+ return startDate .value .ts < d .ts && d .ts < endDate .value .ts
123+ }
124+
125+ const isOpen = ref (false )
126+ const handleOpen = () => {
127+ isOpen .value = true
128+ }
129+ const handleClose = () => {
130+ isOpen .value = false
131+
132+ if (! startDate .value .ts ) {
133+ month .value = currentDate .value .month
134+ year .value = currentDate .value .year
135+ }
136+ }
137+
138+ const handleApply = () => {
139+ isOpen .value = false
140+
141+ if (! endDate .value .ts ) {
142+ endDate .value = startDate .value .endOf (' day' )
143+ }
144+
145+ emit (' onUpdate' , { from: parseInt (startDate .value .ts / 1_000 ), to: parseInt (endDate .value .ts / 1_000 ) })
146+ }
147+
148+ const handleClear = () => {
149+ isOpen .value = false
150+
151+ startDate .value = {}
152+ endDate .value = {}
153+
154+ emit (' onUpdate' , { clear: true })
155+ }
156+
157+ const handleMonthChange = (v ) => {
158+ switch (month .value + v) {
159+ case 0 :
160+ month .value = 12
161+ year .value --
162+ break
163+ case 13 :
164+ month .value = 1
165+ year .value ++
166+ break
167+ default :
168+ month .value += v
169+ }
170+ }
171+
172+ watch (
173+ () => props .from ,
174+ () => {
175+ updateSelectedRange (DateTime .fromSeconds (parseInt (props .from )), DateTime .fromSeconds (parseInt (props .to )))
176+ },
177+ )
178+ < / script>
179+
180+ < template>
181+ < Popover : open= " isOpen" @on- close= " handleClose" width= " 250" >
182+ < Button @click= " handleOpen" type= " secondary" size= " mini" >
183+ < Icon name= " plus-circle" size= " 12" color= " tertiary" / >
184+
185+ < Text color= " secondary" > Date Range < / Text >
186+
187+ < template v- if = " selectedRange" >
188+ < div : class = " $style.vertical_divider" / >
189+
190+ < Text size= " 12" weight= " 600" color= " primary" >
191+ {{ selectedRange }}
192+ < / Text >
193+
194+ < Icon @click .stop = " handleClear" name= " close-circle" size= " 12" color= " secondary" / >
195+ < / template>
196+ < / Button>
197+
198+ < template #content>
199+ < Flex direction= " column" gap= " 12" >
200+ < Flex align= " center" justify= " center" gap= " 6" >
201+ < Icon
202+ @click= " handleMonthChange(-1)"
203+ name= " chevron"
204+ size= " 14"
205+ color= " tertiary"
206+ : class = " !isPrevMonthAvailable && $style.disabled"
207+ : style= " { transform: 'rotate(90deg)' }"
208+ / >
209+
210+ < Text size= " 12" color= " secondary" > {{ ` ${ DateTime .local (year, month).toFormat (' LLLL' )} ${ year} ` }} < / Text >
211+
212+ < Icon
213+ @click= " handleMonthChange(1)"
214+ name= " chevron"
215+ size= " 14"
216+ color= " tertiary"
217+ : class = " !isNextMonthAvailable && $style.disabled"
218+ : style= " { transform: 'rotate(-90deg)' }"
219+ / >
220+ < / Flex>
221+
222+ < Flex direction= " column" gap= " 16" wide : class = " $style.table" >
223+ < table>
224+ < thead>
225+ < tr>
226+ < th v- for = " wd in weekdays" >
227+ < Text size= " 10" color= " secondary" > {{ wd }} < / Text >
228+ < / th>
229+ < / tr>
230+ < / thead>
231+
232+ < tbody>
233+ < tr v- for = " w in days" >
234+ < td v- for = " d in w" : class = " !isDayAvailable(d) && $style.disabled" >
235+ < Flex align= " center" justify= " center"
236+ @click= " handleSelectDate(d)"
237+ : class = " [
238+ $style.day,
239+ (d.ts === startDate.ts || d.ts === endDate.ts) && $style.edgeDate,
240+ isInSelectedPeriod(d) && $style.inSelectedPeriod
241+ ]"
242+ >
243+ < Text size= " 12" color= " primary"
244+ : class = " [
245+ d.month !== month && $style.notInCurrentMonth,
246+ (d.ts === startDate.ts || d.ts === endDate.ts || isInSelectedPeriod(d)) && $style.text_primary
247+ ]"
248+ > {{ d .day }} < / Text >
249+ < / Flex>
250+ < / td>
251+ < / tr>
252+ < / tbody>
253+ < / table>
254+ < / Flex>
255+
256+ < Button @click= " handleApply" type= " secondary" size= " mini" wide> Apply< / Button>
257+ < / Flex>
258+ < / template>
259+ < / Popover>
260+ < / template>
261+
262+ < style module >
263+ .vertical_divider {
264+ min- width: 2px ;
265+ height: 12px ;
266+ background: var (-- op- 10 );
267+ }
268+
269+ .table {
270+ transition: all 0 .2s ease;
271+
272+ & table {
273+ width: 100 % ;
274+ height: fit- content;
275+
276+ border- spacing: 0px ;
277+
278+ & tbody {
279+ & tr {
280+ transition: all 0 .05s ease;
281+ }
282+ }
283+
284+ & tr th {
285+ text- align: center;
286+ padding- bottom: 8px ;
287+ }
288+
289+ & tr td {
290+ text- align: center;
291+
292+ cursor: pointer;
293+ }
294+
295+ th: first- child, td: first- child {
296+ border- radius: 3px ;
297+ }
298+
299+ th: last- child, td: last- child {
300+ border- radius: 3px ;
301+ }
302+ }
303+ }
304+
305+ .day {
306+ min- width: 20px ;
307+ min- height: 20px ;
308+
309+ border- radius: 3px ;
310+ }
311+
312+ .day : hover {
313+ background- color: rgba (51 , 168 , 83 , 70 % );
314+ }
315+
316+ .notInCurrentMonth {
317+ color: var (-- txt- tertiary);
318+ }
319+
320+ .edgeDate {
321+ background- color: rgba (51 , 168 , 83 , 70 % );
322+ }
323+
324+ .inSelectedPeriod {
325+ background- color: var (-- btn- secondary- bg);
326+ }
327+
328+ .text_primary {
329+ color: var (-- txt- primary);
330+ }
331+
332+ .disabled {
333+ opacity: 0.3 ;
334+ pointer- events: none;
335+ cursor: default;
336+ }
337+ < / style>
0 commit comments