@autobusal/common 0.0.17 → 0.0.19

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.
@@ -0,0 +1,105 @@
1
+ import { useState, useRef, ChangeEvent, KeyboardEvent } from 'react';
2
+ import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
3
+ import { TFunction } from 'i18next';
4
+ import { useOutside } from '@autobusal/hooks';
5
+ import { validate } from '@autobusal/utilities';
6
+ import Suggestions from './Suggestions';
7
+ import { Container } from './styles';
8
+ import { AutocompleteData } from './types';
9
+
10
+ interface Props {
11
+ name: 'from' | 'to'
12
+ defaultValue: string
13
+ data: AutocompleteData[]
14
+ validation: string
15
+ t: TFunction<'general'>
16
+ refs: UseFormRegister<any>
17
+ onUpdate: UseFormSetValue<any>
18
+ }
19
+
20
+ const Autocomplete = ({ name, defaultValue, data, validation, t, refs, onUpdate }: Props): JSX.Element => {
21
+ const found = data.filter(item => {
22
+ return item.value === defaultValue;
23
+ });
24
+
25
+ const [ show, setShow ] = useState<boolean>(false);
26
+ const [ q, setQ ] = useState<string>(found.length > 0 ? found[0].name : '');
27
+ const [ selected, setSelected ] = useState<number>(-1);
28
+ const [ value, setValue ] = useState<string>(defaultValue);
29
+
30
+ const ref = useRef(null);
31
+
32
+ useOutside(ref, () => setShow(false));
33
+
34
+ const options = data.filter(item => {
35
+ return item.name.toLowerCase().indexOf(q.toLowerCase()) > -1;
36
+ });
37
+
38
+ const onChange = (event: ChangeEvent<HTMLInputElement>): void => {
39
+ setQ(event.target.value);
40
+
41
+ setShow(true);
42
+ setSelected(-1);
43
+
44
+ setValue('');
45
+ onUpdate(name, '');
46
+ };
47
+
48
+ const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
49
+ if (event.code === 'Escape') {
50
+ setShow(false);
51
+ } else if (event.code === 'ArrowUp') {
52
+ let value = selected - 1;
53
+
54
+ if (value < 0) {
55
+ value = 0;
56
+ }
57
+
58
+ setSelected(value);
59
+ } else if (event.code === 'ArrowDown') {
60
+ let value = selected + 1;
61
+
62
+ if (value > options.length - 1) {
63
+ value = options.length - 1;
64
+ }
65
+
66
+ setSelected(value);
67
+ } else if (event.code === 'Enter') {
68
+ event.preventDefault();
69
+ onSelected(selected);
70
+ }
71
+ };
72
+
73
+ const onSelected = (index: number): void => {
74
+ const selected = options[index];
75
+
76
+ if (selected !== undefined) {
77
+ setShow(false);
78
+ setSelected(-1);
79
+
80
+ setQ(selected.name);
81
+
82
+ setValue(selected.value);
83
+ onUpdate(name, selected.value);
84
+ }
85
+ };
86
+
87
+ return (
88
+ <Container ref={ ref }>
89
+ <input
90
+ type="text"
91
+ name={ `${ name }_slug` }
92
+ autoComplete="off"
93
+ value={ q }
94
+ onChange={ (event) => onChange(event) }
95
+ onKeyDown={ (event) => onKeyDown(event) }
96
+ />
97
+
98
+ { (show && q.length > 1 && options.length > 0) && <Suggestions options={ options } selected={ selected } onClick={ onSelected } /> }
99
+
100
+ <input type="hidden" defaultValue={ value } { ...refs(name, validate(validation, t)) } />
101
+ </Container>
102
+ );
103
+ }
104
+
105
+ export default Autocomplete;
@@ -0,0 +1,24 @@
1
+ import { ContainerSuggestions, ButtonSuggestion } from './styles';
2
+ import { AutocompleteData } from './types';
3
+
4
+ interface Props {
5
+ options: AutocompleteData[]
6
+ selected: number
7
+ onClick: (index: number) => void
8
+ }
9
+
10
+ const Suggestions = ({ options, selected, onClick }: Props): JSX.Element => {
11
+ const items = options.map((item, index) => (
12
+ <ButtonSuggestion key={ index } type="button" $selected={ selected === index } onClick={ () => onClick(index) }>
13
+ { item.name }
14
+ </ButtonSuggestion>
15
+ ));
16
+
17
+ return (
18
+ <ContainerSuggestions>
19
+ { items }
20
+ </ContainerSuggestions>
21
+ );
22
+ };
23
+
24
+ export default Suggestions;
@@ -0,0 +1,38 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ position: relative;
5
+ `;
6
+
7
+ export const ContainerSuggestions = styled.div`
8
+ position: absolute;
9
+ top: 44px;
10
+ left: 0;
11
+ z-index: 100;
12
+ width: 250px;
13
+ max-height: 200px;
14
+ overflow-y: auto;
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 6px;
18
+ padding: 6px;
19
+ background: ${ props => props.theme.foreground.normal };
20
+ box-shadow: ${ props => props.theme.boxShadow };
21
+ border-radius: ${ props => props.theme.borderRadius };
22
+ `;
23
+
24
+ export const ButtonSuggestion = styled.button<{ $selected: boolean }>`
25
+ padding: 6px 12px;
26
+ border-radius: ${ props => props.theme.borderRadius };
27
+ transition: all 0.3s ease;
28
+
29
+ &:hover, &.picked {
30
+ color: ${ props => props.theme.primary.contrast };
31
+ background-color: ${ props => props.theme.primary.normal };
32
+ }
33
+
34
+ ${ props => props.$selected && css`
35
+ color: ${ props => props.theme.primary.contrast };
36
+ background-color: ${ props => props.theme.primary.normal };
37
+ ` }
38
+ `;
@@ -0,0 +1,4 @@
1
+ export interface AutocompleteData {
2
+ name: string
3
+ value: string
4
+ }
package/Button/styles.ts CHANGED
@@ -22,6 +22,7 @@ export const ButtonStyled = styled.button<ButtonType>`
22
22
  justify-content: center;
23
23
  padding: 0 20px;
24
24
  font-weight: 700;
25
+ color: ${ props => props.theme.primary.contrast };
25
26
  background: ${ props => props.theme.primary.normal };
26
27
  border: 1px solid ${ props => props.theme.primary.normal };
27
28
  border-radius: 100px;
@@ -0,0 +1,65 @@
1
+ import { ChangeEvent } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import { AiOutlineArrowLeft, AiOutlineArrowRight } from 'react-icons/ai';
4
+ import { createDate } from '@autobusal/utilities';
5
+ import { getMonths, getYears } from '../utilities/browse';
6
+ import { Container, ButtonBrowse, Choose, Select } from './styles';
7
+
8
+ interface Props {
9
+ type: 'picker' | 'dob' | 'date'
10
+ value: string
11
+ t: TFunction<'general'>
12
+ onChange: (date: Date) => void
13
+ }
14
+
15
+ const Browse = ({ type, value, t, onChange }: Props): JSX.Element => {
16
+ const date = createDate(value);
17
+
18
+ const onPrevious = (): void => {
19
+ date.setMonth(date.getMonth() - 1);
20
+
21
+ onChange(date);
22
+ }
23
+
24
+ const onNext = (): void => {
25
+ date.setMonth(date.getMonth() + 1);
26
+
27
+ onChange(date);
28
+ }
29
+
30
+ const onMonth = (event: ChangeEvent<HTMLSelectElement>): void => {
31
+ date.setMonth(Number(event.target.value));
32
+
33
+ onChange(date);
34
+ };
35
+
36
+ const onYear = (event: ChangeEvent<HTMLSelectElement>): void => {
37
+ date.setFullYear(Number(event.target.value));
38
+
39
+ onChange(date);
40
+ };
41
+
42
+ return (
43
+ <Container>
44
+ <ButtonBrowse type="button" onClick={onPrevious}>
45
+ <AiOutlineArrowLeft />
46
+ </ButtonBrowse>
47
+
48
+ <Choose>
49
+ <Select value={ date.getMonth() } onChange={ onMonth }>
50
+ { getMonths(t) }
51
+ </Select>
52
+
53
+ <Select $type="year" value={ date.getFullYear() } onChange={ onYear }>
54
+ { getYears(type, date.getFullYear()) }
55
+ </Select>
56
+ </Choose>
57
+
58
+ <ButtonBrowse type="button" onClick={onNext}>
59
+ <AiOutlineArrowRight />
60
+ </ButtonBrowse>
61
+ </Container>
62
+ );
63
+ };
64
+
65
+ export default Browse;
@@ -0,0 +1,34 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ display: flex;
5
+ gap: 5px;
6
+ padding-bottom: 5px;
7
+ `;
8
+
9
+ export const ButtonBrowse = styled.button`
10
+ display: flex;
11
+ justify-content: center;
12
+ align-items: center;
13
+ width: 26px;
14
+ height: 26px;
15
+ font-size: ${ props => props.theme.size.s };
16
+ background-color: ${ props => props.theme.background.neutral };
17
+ border-radius: 100px;
18
+ `;
19
+
20
+ export const Choose = styled.div`
21
+ flex: 1;
22
+ display: flex;
23
+ gap: 5px;
24
+ `;
25
+
26
+ export const Select = styled.select<{$type?: 'year'}>`
27
+ height: 26px;
28
+ ${props => props.$type === 'year' && css`
29
+ flex-basis: 90px;
30
+ `}
31
+ text-align: center;
32
+ background-color: ${ props => props.theme.background.neutral };
33
+ border: none;
34
+ `;
@@ -0,0 +1,56 @@
1
+ import { useState, useRef } from 'react';
2
+ import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
3
+ import { TFunction } from 'i18next';
4
+ import { useOutside } from '@autobusal/hooks';
5
+ import { validate, prepareDate } from '@autobusal/utilities';
6
+ import Picker from './Picker';
7
+ import { Container } from './styles';
8
+
9
+ interface Props {
10
+ type: 'picker' | 'dob' | 'date'
11
+ name: string
12
+ defaultValue?: string
13
+ t: TFunction<'general'>
14
+ validation?: string
15
+ refs: UseFormRegister<any>
16
+ onUpdate: UseFormSetValue<any>
17
+ }
18
+
19
+ const Calendar = ({ type, name, defaultValue, t, validation, refs, onUpdate }: Props): JSX.Element => {
20
+ const [ show, setShow ] = useState<boolean>(false);
21
+ const [ prepared, setPrepared ] = useState<string>(defaultValue ?? prepareDate(new Date()));
22
+ const [ selected, setSelected ] = useState<string>(prepared);
23
+
24
+ const ref = useRef(null);
25
+
26
+ useOutside(ref, () => setShow(false));
27
+
28
+ const onClick = (prepared: string): void => {
29
+ setShow(false);
30
+
31
+ setSelected(prepared);
32
+ setPrepared(prepared);
33
+
34
+ onUpdate(name, prepared);
35
+ };
36
+
37
+ return (
38
+ <Container ref={ ref }>
39
+ <input type="text" autoComplete="off" readOnly={ true } value={ prepared } onClick={ () => setShow(true) } />
40
+
41
+ { show && (
42
+ <Picker
43
+ type={ type }
44
+ value={ prepared }
45
+ selected={ selected }
46
+ t={ t }
47
+ onClick={ onClick }
48
+ />
49
+ ) }
50
+
51
+ <input type="hidden" defaultValue={ prepared } { ...refs(name, validate(validation ?? '', t)) } />
52
+ </Container>
53
+ );
54
+ }
55
+
56
+ export default Calendar;
@@ -0,0 +1,101 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { createDate, prepareDate } from '@autobusal/utilities';
3
+ import { getCalendar, getLimits, compareDates } from './utilities/settings';
4
+ import { ContainerDays, Day } from './styles';
5
+ import { CalendarData } from './types';
6
+
7
+ interface Props {
8
+ type: 'picker' | 'dob' | 'date'
9
+ value: string
10
+ selected: string
11
+ onSelected: (prepared: string, disabled: boolean) => void
12
+ }
13
+
14
+ const Days = ({ type, value, selected, onSelected }: Props): JSX.Element => {
15
+ const [ calendar, setCalendar ] = useState<CalendarData>(getCalendar(value));
16
+
17
+ useEffect(() => {
18
+ setCalendar(getCalendar(value));
19
+ }, [value]);
20
+
21
+ const today = prepareDate(new Date());
22
+
23
+ const limits = getLimits(type);
24
+
25
+ const rows: JSX.Element[] = [];
26
+
27
+ let currentDay: number = 1;
28
+
29
+ // prepare the previous date
30
+ const previousDate: Date = createDate(value);
31
+ previousDate.setMonth(previousDate.getMonth() - 1);
32
+
33
+ // setup the current date
34
+ const currentDate: Date = createDate(value);
35
+
36
+ // prepare the next date
37
+ const nextDate: Date = createDate(value);
38
+ nextDate.setMonth(nextDate.getMonth() + 1);
39
+
40
+ for (let i = 1; i <= calendar.weeks; i++) {
41
+ for (let j = 1; j <= 7; j++) {
42
+ const key = i * 10 + j;
43
+
44
+ if (i == 1 && j < calendar.begins) {
45
+ previousDate.setDate(calendar.previous);
46
+
47
+ const disabled = compareDates(previousDate, limits);
48
+
49
+ rows.push(
50
+ <Day key={ key } $type="other" $disabled={ disabled }>
51
+ <span>{ calendar.previous }</span>
52
+ </Day>
53
+ );
54
+
55
+ calendar.previous++;
56
+ } else if (currentDay > calendar.month) {
57
+ nextDate.setDate(calendar.next);
58
+
59
+ const disabled = compareDates(nextDate, limits);
60
+
61
+ rows.push(
62
+ <Day key={ key } $type="other" $disabled={ disabled }>
63
+ <span>{ calendar.next }</span>
64
+ </Day>
65
+ );
66
+
67
+ calendar.next++;
68
+ } else {
69
+ currentDate.setDate(currentDay);
70
+
71
+ // we create a new date to use for the compare check
72
+ const ourDate = new Date(currentDate);
73
+
74
+ const disabled = compareDates(ourDate, limits);
75
+
76
+ const currentValue = prepareDate(ourDate);
77
+
78
+ rows.push(
79
+ <Day key={ key }
80
+ $type={ today === currentValue ? 'today' : undefined }
81
+ $disabled={ disabled }
82
+ $selected={ selected === currentValue }
83
+ onClick={ () => onSelected(currentValue, disabled) }
84
+ >
85
+ <span>{ currentDay }</span>
86
+ </Day>
87
+ );
88
+
89
+ currentDay++;
90
+ }
91
+ }
92
+ }
93
+
94
+ return (
95
+ <ContainerDays>
96
+ { rows }
97
+ </ContainerDays>
98
+ );
99
+ };
100
+
101
+ export default Days;
@@ -0,0 +1,30 @@
1
+ import { TFunction } from 'i18next';
2
+ import { Container, HeaderDay } from './styles';
3
+
4
+ interface Props {
5
+ t: TFunction<'common'>
6
+ }
7
+
8
+ const Header = ({ t }: Props): JSX.Element => {
9
+ const days = [
10
+ t('data.days.mon', { ns: 'common' }),
11
+ t('data.days.tue', { ns: 'common' }),
12
+ t('data.days.wed', { ns: 'common' }),
13
+ t('data.days.thu', { ns: 'common' }),
14
+ t('data.days.fri', { ns: 'common' }),
15
+ t('data.days.sat', { ns: 'common' }),
16
+ t('data.days.sun', { ns: 'common' })
17
+ ];
18
+
19
+ const items = days.map((item, index) => (
20
+ <HeaderDay key={ index }>{ item }</HeaderDay>
21
+ ));
22
+
23
+ return (
24
+ <Container>
25
+ { items }
26
+ </Container>
27
+ );
28
+ };
29
+
30
+ export default Header;
@@ -0,0 +1,15 @@
1
+ import styled from 'styled-components';
2
+ import { Day } from '../styles';
3
+
4
+ export const Container = styled.div`
5
+ display: flex;
6
+ `;
7
+
8
+ export const HeaderDay = styled(Day)`
9
+ justify-content: center;
10
+ font-size: ${ props => props.theme.size.xs };
11
+ color: ${ props => props.theme.font.faded };
12
+ font-weight: 700;
13
+ text-transform: uppercase;
14
+ cursor: default;
15
+ `;
@@ -0,0 +1,45 @@
1
+ import { useState } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import { prepareDate } from '@autobusal/utilities';
4
+ import Browse from './Browse/Browse';
5
+ import Header from './Header/Header';
6
+ import Days from './Days';
7
+ import { ContainerPicker } from './styles';
8
+
9
+ interface Props {
10
+ type: 'picker' | 'dob' | 'date'
11
+ value: string
12
+ selected: string
13
+ t: TFunction<'general'>
14
+ onClick: (prepared: string) => void
15
+ }
16
+
17
+ const Picker = ({ type, value, selected, t, onClick }: Props): (JSX.Element | null) => {
18
+ const [ prepared, setPrepared ] = useState<string>(value);
19
+
20
+ const onSelected = (prepared: string, disabled: boolean): void => {
21
+ if (disabled) {
22
+ return alert(t('general:errors.invalid.date'));
23
+ }
24
+
25
+ onClick(prepared);
26
+ };
27
+
28
+ const onChange = (newDate: Date): void => {
29
+ setPrepared(
30
+ prepareDate(newDate)
31
+ );
32
+ };
33
+
34
+ return (
35
+ <ContainerPicker>
36
+ <Browse type={ type } value={ prepared } t={ t } onChange={ onChange } />
37
+
38
+ <Header t={ t } />
39
+
40
+ <Days type={ type } value={ prepared } selected={ selected } onSelected={ onSelected } />
41
+ </ContainerPicker>
42
+ );
43
+ };
44
+
45
+ export default Picker;
@@ -0,0 +1,69 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ position: relative;
5
+ `;
6
+
7
+ export const ContainerPicker = styled.div`
8
+ position: absolute;
9
+ top: 44px;
10
+ left: 0;
11
+ z-index: 100;
12
+ width: 276px;
13
+ padding: 5px;
14
+ background-color: ${ props => props.theme.foreground.normal };
15
+ box-shadow: ${ props => props.theme.boxShadow };
16
+ border-radius: ${ props => props.theme.borderRadius };
17
+ `;
18
+
19
+ export const ContainerDays = styled.div`
20
+ display: flex;
21
+ flex-wrap: wrap;
22
+ `;
23
+
24
+ export const Day = styled.div<{
25
+ $type?: ('today' | 'other')
26
+ $disabled?: boolean
27
+ $selected?: boolean
28
+ }>`
29
+ display: flex;
30
+ width: calc(100% / 7);
31
+ padding: 2px;
32
+ cursor: pointer;
33
+
34
+ & > span {
35
+ width: 26px;
36
+ height: 26px;
37
+ margin: 0 auto;
38
+ font-size: ${ props => props.theme.size.xs };
39
+ line-height: 26px;
40
+ text-align: center;
41
+ border-radius: 100px;
42
+ transition: all 0.3s ease;
43
+ }
44
+
45
+ ${ props => props.$type === 'other' && css`
46
+ color: ${ props => props.theme.font.faded };
47
+ ` }
48
+
49
+ ${ props => props.$type === 'today' && css`
50
+ font-weight: 700;
51
+ color: ${ props => props.theme.primary.normal };
52
+ ` }
53
+
54
+ ${ props => props.$disabled === true && css`
55
+ opacity: .3;
56
+ ` }
57
+
58
+ ${ props => props.$selected && css`
59
+ & > span {
60
+ font-weight: 700;
61
+ color: ${ props => props.theme.primary.contrast };
62
+ background-color: ${ props => props.theme.primary.normal };
63
+ }
64
+ ` }
65
+
66
+ &:hover > span {
67
+ background-color: ${ props => props.theme.background.neutral };
68
+ }
69
+ `;
@@ -0,0 +1,28 @@
1
+ export interface CalendarData {
2
+ begins: number
3
+ month: number
4
+ previous: number
5
+ next: number
6
+ weeks: number
7
+ }
8
+
9
+ export interface DateLimit {
10
+ year: number
11
+ month: number
12
+ day: number
13
+ }
14
+
15
+ export interface DatesLimit {
16
+ min: DateLimit
17
+ max: DateLimit
18
+ }
19
+
20
+ export interface ReturnLimit {
21
+ min: number
22
+ max: number
23
+ }
24
+
25
+ export interface CalendarLimit {
26
+ year: ReturnLimit
27
+ time: ReturnLimit
28
+ }
@@ -0,0 +1,53 @@
1
+ import { TFunction } from 'i18next';
2
+ import { getLimits } from './settings';
3
+
4
+ export const getMonths = (t: TFunction<'general'>): JSX.Element[] => {
5
+ const months = [
6
+ t('data.months.january', { ns: 'common' }),
7
+ t('data.months.february', { ns: 'common' }),
8
+ t('data.months.march', { ns: 'common' }),
9
+ t('data.months.april', { ns: 'common' }),
10
+ t('data.months.may', { ns: 'common' }),
11
+ t('data.months.june', { ns: 'common' }),
12
+ t('data.months.july', { ns: 'common' }),
13
+ t('data.months.august', { ns: 'common' }),
14
+ t('data.months.september', { ns: 'common' }),
15
+ t('data.months.october', { ns: 'common' }),
16
+ t('data.months.november', { ns: 'common' }),
17
+ t('data.months.december', { ns: 'common' })
18
+ ];
19
+
20
+ const options: JSX.Element[] = [];
21
+
22
+ months.forEach((month, index) => (
23
+ options.push(<option key={ index } value={ index }>{ month }</option>)
24
+ ));
25
+
26
+ return options;
27
+ };
28
+
29
+ export const getYears = (type: ('picker' | 'dob' | 'date'), currentYear: number): JSX.Element[] => {
30
+ const limits = getLimits(type);
31
+
32
+ const current = new Date().getFullYear();
33
+ const start = current - limits.year.min;
34
+ const end = current + limits.year.max;
35
+
36
+ const options: JSX.Element[] = [];
37
+
38
+ // add dummy year if browsing
39
+ if (currentYear < start) {
40
+ options.push(<option key={ currentYear } value={ currentYear }>{ currentYear }</option>);
41
+ }
42
+
43
+ for (let i = start; i <= end; i++) {
44
+ options.push(<option key={ i } value={ i }>{ i }</option>);
45
+ }
46
+
47
+ // add dummy year if browsing
48
+ if (currentYear > end) {
49
+ options.push(<option key={ currentYear } value={ currentYear }>{ currentYear }</option>);
50
+ }
51
+
52
+ return options;
53
+ };
@@ -0,0 +1,86 @@
1
+ import { createDate } from '@autobusal/utilities';
2
+ import { CalendarLimit, DatesLimit, CalendarData } from '../types';
3
+
4
+ export const getLimits = (type: ('picker' | 'dob' | 'date')): CalendarLimit => {
5
+ const today = new Date();
6
+
7
+ today.setHours(0, 0, 0, 0);
8
+
9
+ const limits: DatesLimit = {
10
+ min: {
11
+ year: 0,
12
+ month: 0,
13
+ day: 1
14
+ },
15
+ max: {
16
+ year: 0,
17
+ month: 11,
18
+ day: 31
19
+ },
20
+ };
21
+
22
+ if (type === 'dob') {
23
+ limits.min.year = 100;
24
+
25
+ limits.max.month = today.getMonth();
26
+ limits.max.day = today.getDate();
27
+ }
28
+
29
+ if (type === 'picker') {
30
+ limits.min.month = today.getMonth();
31
+ limits.min.day = today.getDate();
32
+
33
+ limits.max.year = 1;
34
+ }
35
+
36
+ if (type == 'date') {
37
+ limits.min.year = 5;
38
+ limits.max.year = 0;
39
+ }
40
+
41
+ return {
42
+ year: {
43
+ min: limits.min.year,
44
+ max: limits.max.year
45
+ },
46
+
47
+ time: {
48
+ min: new Date(today.getFullYear() - limits.min.year, limits.min.month, limits.min.day, 0, 0, 0, 0).getTime(),
49
+ max: new Date(today.getFullYear() + limits.max.year, limits.max.month, limits.max.day, 0, 0, 0, 0).getTime()
50
+ }
51
+ };
52
+ };
53
+
54
+ export const getCalendar = (value: string): CalendarData => {
55
+ const prepared = createDate(value);
56
+
57
+ // when the month begins (when in the week)
58
+ let begins = new Date(prepared.getFullYear(), prepared.getMonth(), 1).getDay();
59
+ begins = begins === 0 ? 7 : begins; // move Sunday to be the 7th day
60
+
61
+ // get the previous month
62
+ const previous = new Date(prepared.getFullYear(), (prepared.getMonth() - 1), 0).getDate(); // the amount of days in the previous month
63
+
64
+ const calendar: CalendarData = {
65
+ begins: begins,
66
+ month: new Date(prepared.getFullYear(), (prepared.getMonth() + 1), 0).getDate(), // the amount of days in a month
67
+ previous: previous - begins + 2,
68
+ next: 1,
69
+ weeks: 5
70
+ };
71
+
72
+ // const total = calendar.first_day + calendar.month_days - 1;
73
+ const total = calendar.begins + calendar.month - 1;
74
+
75
+ if (total > 35) {
76
+ calendar.weeks = 6;
77
+ } else if (total <= 28) {
78
+ calendar.weeks = 4;
79
+ }
80
+
81
+ return calendar;
82
+ };
83
+
84
+ export const compareDates = (date: Date, limits: CalendarLimit): boolean => (
85
+ date.getTime() < limits.time.min || date.getTime() > limits.time.max
86
+ );
@@ -48,6 +48,7 @@ const PageCss = css<LinkType>`
48
48
 
49
49
  ${ props => props.$active && css`
50
50
  font-weight: 700;
51
+ color: ${ props => props.theme.primary.contrast } !important;
51
52
  background: ${ props => props.theme.primary.normal } !important;
52
53
  ` }
53
54
  `;
@@ -0,0 +1,74 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
4
+ import { useOutside } from '@autobusal/hooks';
5
+ import getText from './utilities/getText';
6
+ import Persons from './Persons';
7
+ import { Container } from './styles';
8
+
9
+ interface Props {
10
+ defaultAdults: number
11
+ defaultChildren: number
12
+ defaultBabies: number
13
+ t: TFunction<'common'>
14
+ refs: UseFormRegister<any>
15
+ onUpdate: UseFormSetValue<any>
16
+ }
17
+
18
+ const Passengers = ({ defaultAdults, defaultChildren, defaultBabies, t, refs, onUpdate }: Props): JSX.Element => {
19
+ const [ show, setShow ] = useState<boolean>(false);
20
+ const [ adults, setAdults ] = useState<number>(defaultAdults);
21
+ const [ children, setChildren ] = useState<number>(defaultChildren);
22
+ const [ babies, setBabies ] = useState<number>(defaultBabies);
23
+ const [ display, setDisplay ] = useState(
24
+ getText(adults, children, babies, t)
25
+ );
26
+
27
+ const ref = useRef(null);
28
+
29
+ useEffect(() => {
30
+ const text = getText(adults, children, babies, t);
31
+ setDisplay(text);
32
+ }, [adults, children, babies]);
33
+
34
+ useOutside(ref, () => setShow(false));
35
+
36
+ const onChange = (name: ('adults' | 'children' | 'babies'), amount: number): void => {
37
+ switch (name) {
38
+ case 'adults':
39
+ setAdults(amount);
40
+ break;
41
+
42
+ case 'children':
43
+ setChildren(amount);
44
+ break;
45
+
46
+ case 'babies':
47
+ setBabies(amount);
48
+ break;
49
+ }
50
+
51
+ onUpdate(name, amount);
52
+ };
53
+
54
+ return (
55
+ <Container ref={ ref }>
56
+ <input type="text" value={ display } readOnly={ true } onClick={ () => setShow(!show) } />
57
+
58
+ { show &&
59
+ <Persons
60
+ adults={ adults }
61
+ children={ children }
62
+ babies={ babies }
63
+ t={ t }
64
+ onChange={ onChange }
65
+ /> }
66
+
67
+ <input type="hidden" defaultValue={ adults } { ...refs('adults') } />
68
+ <input type="hidden" defaultValue={ children } { ...refs('children') } />
69
+ <input type="hidden" defaultValue={ babies } { ...refs('babies') } />
70
+ </Container>
71
+ );
72
+ };
73
+
74
+ export default Passengers;
@@ -0,0 +1,48 @@
1
+ import { useState } from 'react';
2
+ import { FaMinus, FaPlus } from 'react-icons/fa';
3
+ import { Choose, About, Options, Title, Description, Number, ButtonAmount } from './styles';
4
+
5
+ interface Props {
6
+ name: 'adults' | 'children' | 'babies'
7
+ title: string
8
+ description: string
9
+ amount: number
10
+ onChange: (name: ('adults' | 'babies' | 'children'), amount: number) => void
11
+ }
12
+
13
+ const Person = ({ name, title, description, amount, onChange }: Props): JSX.Element => {
14
+ const [ number, setNumber ] = useState<number>(amount);
15
+
16
+ const onClick = (type: ('minus' | 'plus')): void => {
17
+ let value = type === 'minus' ? (number - 1) : (number + 1);
18
+
19
+ if (value < 0) {
20
+ value = 0;
21
+ } else if (value > 6) {
22
+ value = 6;
23
+ }
24
+
25
+ setNumber(value);
26
+
27
+ onChange(name, value);
28
+ };
29
+
30
+ return (
31
+ <Choose>
32
+ <About>
33
+ <Title>{ title }</Title>
34
+ <Description>{ description }</Description>
35
+ </About>
36
+
37
+ <Options>
38
+ <ButtonAmount type="button" onClick={ () => onClick('minus') }><FaMinus /></ButtonAmount>
39
+
40
+ <Number>{ number }</Number>
41
+
42
+ <ButtonAmount type="button" onClick={ () => onClick('plus') }><FaPlus /></ButtonAmount>
43
+ </Options>
44
+ </Choose>
45
+ );
46
+ }
47
+
48
+ export default Person;
@@ -0,0 +1,41 @@
1
+ import { TFunction } from 'i18next';
2
+ import Person from './Person';
3
+ import { Passengers } from './styles';
4
+
5
+ interface Props {
6
+ adults: number
7
+ children: number
8
+ babies: number
9
+ t: TFunction<'common'>
10
+ onChange: (name: ('adults' | 'children' | 'babies'), amount: number) => void
11
+ }
12
+
13
+ const Persons = ({ adults, children, babies, t, onChange }: Props): JSX.Element => (
14
+ <Passengers>
15
+ <Person
16
+ name="adults"
17
+ title={ t('data.persons.adult.plural', { ns: 'common' }) }
18
+ description={ t('data.persons.adult.description', { ns: 'common' }) }
19
+ amount={ adults }
20
+ onChange={ onChange }
21
+ />
22
+
23
+ <Person
24
+ name="children"
25
+ title={ t('data.persons.child.plural', { ns: 'common' }) }
26
+ description={ t('data.persons.child.description', { ns: 'common' }) }
27
+ amount={ children }
28
+ onChange={ onChange }
29
+ />
30
+
31
+ <Person
32
+ name="babies"
33
+ title={ t('data.persons.baby.plural', { ns: 'common' }) }
34
+ description={ t('data.persons.baby.description', { ns: 'common' }) }
35
+ amount={ babies }
36
+ onChange={ onChange }
37
+ />
38
+ </Passengers>
39
+ );
40
+
41
+ export default Persons;
@@ -0,0 +1,70 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ position: relative;
5
+ `;
6
+
7
+ export const Passengers = styled.div`
8
+ position: absolute;
9
+ top: 44px;
10
+ left: 0;
11
+ z-index: 100;
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: 15px;
15
+ width: 275px;
16
+ padding: 15px;
17
+ background-color: ${ props => props.theme.foreground.normal };
18
+ box-shadow: ${ props => props.theme.boxShadow };
19
+ border-radius: ${ props => props.theme.borderRadius };
20
+
21
+ @media (min-width: 640px) {
22
+ width: 300px;
23
+ }
24
+ `;
25
+
26
+ export const Choose = styled.div`
27
+ display: flex;
28
+ align-items: center;
29
+ `;
30
+
31
+ export const About = styled.div`
32
+ line-height: 1.2;
33
+ `;
34
+
35
+ export const Title = styled.strong`
36
+ display: block;
37
+ `;
38
+
39
+ export const Description = styled.span`
40
+ font-size: ${ props => props.theme.size.s };
41
+ color: ${ props => props.theme.font.faded };
42
+ `;
43
+
44
+ export const Options = styled.div`
45
+ margin-left: auto;
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 4px;
49
+ `;
50
+
51
+ const NumberAndButtons = css`
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ width: 36px;
56
+ height: 36px;
57
+ background: ${ props => props.theme.background.neutral };
58
+ border-radius: ${ props => props.theme.borderRadius };
59
+ `;
60
+
61
+ export const Number = styled.span`
62
+ ${ NumberAndButtons }
63
+ width: 50px;
64
+ font-weight: 700;
65
+ font-size: ${ props => props.theme.size.l };
66
+ `;
67
+
68
+ export const ButtonAmount = styled.button`
69
+ ${ NumberAndButtons }
70
+ `;
@@ -0,0 +1,21 @@
1
+ import { TFunction } from 'i18next';
2
+
3
+ const getText = (adults: number, children: number, babies: number, t: TFunction<'common'>): string => {
4
+ let final = [];
5
+
6
+ if (adults > 0) {
7
+ final.push(`${ t('data.persons.adult.plural', { ns: 'common' }) }: ${ adults }`);
8
+ }
9
+
10
+ if (children > 0) {
11
+ final.push(`${ t('data.persons.child.plural', { ns: 'common' }) }: ${ children }`);
12
+ }
13
+
14
+ if (babies > 0) {
15
+ final.push(`${ t('data.persons.baby.plural', { ns: 'common' }) }: ${ babies }`);
16
+ }
17
+
18
+ return final.join(', ');
19
+ };
20
+
21
+ export default getText;
@@ -162,6 +162,7 @@ export const LinkOrder = styled(Link)<{ $type: 'normal' | 'small' }>`
162
162
  transition: all 0.3s ease;
163
163
 
164
164
  ${ props => props.$type === 'normal' && css`
165
+ color: ${ props => props.theme.primary.contrast };
165
166
  background-color: ${ props => props.theme.primary.normal };
166
167
  ` }
167
168
 
package/index.ts CHANGED
@@ -1,20 +1,28 @@
1
+ import Autocomplete from './Autocomplete/Autocomplete';
1
2
  import BackTo from './BackTo';
3
+ import Calendar from './Calendar/Calendar';
2
4
  import CookieNotification from './CookieNotification/CookieNotification';
3
5
  import ChangeLanguage from './ChangeLanguage/ChangeLanguage';
4
6
  import { Button } from './Button/Button';
5
7
  import { General, Paragraph } from './Loading/Loading';
6
8
  import Modal from './Modal/Modal';
7
9
  import Meta from './Meta';
10
+ import Pagination from './Pagination/Pagination';
11
+ import Passengers from './Passengers/Passengers';
8
12
  import RouteItem from './RouteItem/RouteItem';
9
13
 
10
14
  export {
15
+ Autocomplete,
11
16
  BackTo,
12
17
  Button,
18
+ Calendar,
13
19
  CookieNotification,
14
20
  ChangeLanguage,
15
21
  General,
16
22
  Meta,
17
23
  Modal,
24
+ Pagination,
18
25
  Paragraph,
26
+ Passengers,
19
27
  RouteItem
20
28
  };
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "dependencies": {
7
7
  "@autobusal/hooks": "^0.0.3",
8
8
  "@autobusal/providers": "^1.0.16",
9
+ "@autobusal/utilities": "^0.0.3",
9
10
  "i18next": "^23.7.18",
10
11
  "react": "^18.2.0",
11
12
  "react-helmet": "^6.1.0",
13
+ "react-hook-form": "^7.49.3",
12
14
  "react-icons": "^5.0.1",
13
15
  "react-loading-skeleton": "^3.3.1",
14
16
  "react-router-dom": "^6.21.3",