mirror of
https://github.com/tomru/pfadi-bussle.git
synced 2026-03-04 15:07:13 +01:00
Merge pull request #3 from tomru/react-day-picker
move to react-day-picker
This commit is contained in:
@@ -2,12 +2,10 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es2020": true,
|
"es2020": true,
|
||||||
"node": true
|
"node": true,
|
||||||
|
"jest": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": ["eslint:recommended", "plugin:react/recommended"],
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
"jsx": true
|
"jsx": true
|
||||||
@@ -15,9 +13,6 @@
|
|||||||
"ecmaVersion": 11,
|
"ecmaVersion": 11,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": ["react"],
|
||||||
"react"
|
"rules": {}
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { WizardContext, ACTIONS } from '../context/wizardStore'
|
import { WizardContext } from '../context/wizardStore'
|
||||||
import Required from './required'
|
import Required from './required'
|
||||||
|
|
||||||
import Form from 'react-bootstrap/Form'
|
import Form from 'react-bootstrap/Form'
|
||||||
|
|||||||
@@ -1,35 +1,69 @@
|
|||||||
import { useContext, useState } from 'react'
|
import React, { useContext, useState, useRef, useEffect } from 'react'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
|
|
||||||
import { WizardContext, ACTIONS } from '../context/wizardStore'
|
import { WizardContext } from '../context/wizardStore'
|
||||||
|
|
||||||
import Form from 'react-bootstrap/Form'
|
import Form from 'react-bootstrap/Form'
|
||||||
|
import Button from 'react-bootstrap/Button'
|
||||||
|
|
||||||
import moment from 'moment'
|
import { DateUtils } from 'react-day-picker'
|
||||||
import 'react-dates/initialize'
|
import DayPickerInput from 'react-day-picker/DayPickerInput'
|
||||||
import { DateRangePicker, SingleDatePicker } from 'react-dates'
|
|
||||||
|
|
||||||
import Required from './required'
|
import Required from './required'
|
||||||
import { dateFormat } from '../helpers/date'
|
import { dateFormat } from '../helpers/date'
|
||||||
|
import { getNextSmaller, getNextBigger } from '../helpers/array'
|
||||||
|
|
||||||
const fetcher = (path) => fetch(path).then((r) => r.json())
|
const fetcher = (path) => fetch(path).then((r) => r.json())
|
||||||
|
|
||||||
export default function DateSelect() {
|
export default function DateSelect() {
|
||||||
const [focusedInput, setFocusedInput] = useState(null)
|
|
||||||
const { state, onChange } = useContext(WizardContext)
|
const { state, onChange } = useContext(WizardContext)
|
||||||
|
const [range, setRange] = useState({
|
||||||
|
form: state.startDate && new Date(state.StartDate),
|
||||||
|
to: state.endDate && new Date(state.endDate),
|
||||||
|
})
|
||||||
|
const { from, to } = range
|
||||||
const { data: daysBooked, error: fetchBookedOnError } = useSWR(
|
const { data: daysBooked, error: fetchBookedOnError } = useSWR(
|
||||||
'/api/daysbooked',
|
'/api/daysbooked',
|
||||||
fetcher
|
fetcher
|
||||||
)
|
)
|
||||||
|
const prevBookedDay = getNextSmaller(daysBooked, dateFormat(from || to))
|
||||||
|
const nextBookedDay = getNextBigger(daysBooked, dateFormat(from || to))
|
||||||
|
|
||||||
const { multipleDays, startDate, endDate } = state.formData
|
const fromRef = useRef()
|
||||||
|
const toRef = useRef()
|
||||||
|
|
||||||
function isDayBlocked(momentDay) {
|
function dayBooked(day) {
|
||||||
|
return daysBooked && daysBooked.includes(dateFormat(day))
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayDisabled(day) {
|
||||||
return (
|
return (
|
||||||
daysBooked && daysBooked.some((rawDay) => momentDay.isSame(rawDay, 'day'))
|
DateUtils.isPastDay(day) ||
|
||||||
|
dayBooked(day) ||
|
||||||
|
(prevBookedDay && day < new Date(prevBookedDay)) ||
|
||||||
|
(nextBookedDay && day > new Date(nextBookedDay))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDate(value) {
|
||||||
|
return new Date(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
toRef.current?.getInput().focus()
|
||||||
|
}, [from])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange({ startDate: from?.toISOString(), endDate: to?.toISOString() })
|
||||||
|
}, [from, to])
|
||||||
|
|
||||||
|
const disabledDays = [dayDisabled]
|
||||||
|
const modifiers = {
|
||||||
|
dayBooked,
|
||||||
|
start: from,
|
||||||
|
end: to,
|
||||||
|
}
|
||||||
|
|
||||||
if (fetchBookedOnError) {
|
if (fetchBookedOnError) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -40,92 +74,55 @@ export default function DateSelect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Form.Group controlId="dateSelect">
|
|
||||||
<Form.Label>
|
|
||||||
Willst du einen odere mehrere Tage buchen? <Required />
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Check
|
|
||||||
type="radio"
|
|
||||||
id={'multipleDays-single'}
|
|
||||||
label="Einen Tag"
|
|
||||||
name="multipleDays"
|
|
||||||
value="single"
|
|
||||||
checked={multipleDays === false}
|
|
||||||
onChange={() => {
|
|
||||||
setFocusedInput(null)
|
|
||||||
onChange({
|
|
||||||
multipleDays: false,
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
type="radio"
|
|
||||||
id={'multipleDays-multiple'}
|
|
||||||
label="Mehrere Tage"
|
|
||||||
name="multipleDays"
|
|
||||||
value="multiple"
|
|
||||||
checked={multipleDays === true}
|
|
||||||
onChange={() => {
|
|
||||||
setFocusedInput(null)
|
|
||||||
onChange({
|
|
||||||
multipleDays: true,
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
{multipleDays !== null && (
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Label component="legend" style={{ display: 'block' }}>
|
<Form.Label component="legend" style={{ display: 'block' }}>
|
||||||
Datum <Required />
|
Datum <Required />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
{multipleDays === false && (
|
<Form.Group>
|
||||||
<SingleDatePicker
|
<DayPickerInput
|
||||||
small={true}
|
ref={fromRef}
|
||||||
date={startDate && moment(startDate)}
|
component={Form.Control}
|
||||||
placeholder="Datum"
|
value={from}
|
||||||
numberOfMonths={1}
|
placeholder="Von"
|
||||||
required
|
formatDate={dateFormat}
|
||||||
onDateChange={(date) =>
|
parseDate={parseDate}
|
||||||
onChange({
|
dayPickerProps={{
|
||||||
startDate: date && dateFormat(date),
|
className: 'Selectable',
|
||||||
})
|
selectedDays: [from, { from, to }],
|
||||||
}
|
disabledDays,
|
||||||
focused={typeof focusedInput === 'boolean' && focusedInput}
|
modifiers,
|
||||||
onFocusChange={({ focused }) => setFocusedInput(focused)}
|
numberOfMonths: 1,
|
||||||
isDayBlocked={isDayBlocked}
|
|
||||||
id="startDate"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{multipleDays === true && (
|
|
||||||
<DateRangePicker
|
|
||||||
small={true}
|
|
||||||
startDate={startDate && moment(startDate)}
|
|
||||||
startDateId="startDate"
|
|
||||||
startDatePlaceholderText="Start"
|
|
||||||
endDatePlaceholderText="Ende"
|
|
||||||
required
|
|
||||||
numberOfMonths={1}
|
|
||||||
endDate={endDate && moment(endDate)}
|
|
||||||
endDateId="endDate"
|
|
||||||
onDatesChange={({ startDate, endDate }) => {
|
|
||||||
onChange({
|
|
||||||
startDate: startDate && dateFormat(startDate),
|
|
||||||
endDate: endDate && dateFormat(endDate),
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
focusedInput={focusedInput}
|
onDayChange={(from) => setRange({ ...range, from })}
|
||||||
onFocusChange={(focusedInput) => setFocusedInput(focusedInput)}
|
|
||||||
isDayBlocked={isDayBlocked}
|
|
||||||
minDate={moment()}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<Form.Label className="px-2">bis</Form.Label>
|
||||||
|
<DayPickerInput
|
||||||
|
ref={toRef}
|
||||||
|
component={Form.Control}
|
||||||
|
inputProps={{ disabled: !from }}
|
||||||
|
value={to}
|
||||||
|
placeholder="Bis"
|
||||||
|
formatDate={dateFormat}
|
||||||
|
parseDate={parseDate}
|
||||||
|
dayPickerProps={{
|
||||||
|
className: 'Selectable',
|
||||||
|
selectedDays: [from, { from, to }],
|
||||||
|
disabledDays,
|
||||||
|
modifiers,
|
||||||
|
numberOfMonths: 1,
|
||||||
|
month: from,
|
||||||
|
fromMonth: from,
|
||||||
|
}}
|
||||||
|
onDayChange={(to) => setRange({ ...range, to })}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="ml-2"
|
||||||
|
variant="outline-secondary"
|
||||||
|
onClick={() => setRange({})}
|
||||||
|
>
|
||||||
|
Zurücksetzen
|
||||||
|
</Button>
|
||||||
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { WizardContext, ACTIONS } from '../context/wizardStore'
|
import { WizardContext } from '../context/wizardStore'
|
||||||
import Required from './required'
|
import Required from './required'
|
||||||
|
|
||||||
import Form from 'react-bootstrap/Form'
|
import Form from 'react-bootstrap/Form'
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
const Required = () => <span>*</span>
|
const Required = () => <span>*</span>
|
||||||
export default Required
|
export default Required
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
|
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import Form from 'react-bootstrap/Form'
|
import Form from 'react-bootstrap/Form'
|
||||||
@@ -28,9 +28,6 @@ function WizardInternal() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = (payload) =>
|
|
||||||
dispatch({ action: ACTIONS.SET_FORM_DATA, payload })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const initialState = {
|
|||||||
postDataError: null,
|
postDataError: null,
|
||||||
postDataSuccess: null,
|
postDataSuccess: null,
|
||||||
formData: {
|
formData: {
|
||||||
multipleDays: true,
|
|
||||||
//startDate: '2020-08-10',
|
//startDate: '2020-08-10',
|
||||||
//endDate: '2020-08-17',
|
//endDate: '2020-08-17',
|
||||||
//purpose: 'Sommerlager 2021',
|
//purpose: 'Sommerlager 2021',
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ export async function getBookedDays() {
|
|||||||
'startDate endDate'
|
'startDate endDate'
|
||||||
).exec()
|
).exec()
|
||||||
|
|
||||||
return bookings.map((booking) => booking.days).flat()
|
return bookings
|
||||||
|
.map((booking) => booking.days)
|
||||||
|
.flat()
|
||||||
|
.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBooking({
|
export async function createBooking({
|
||||||
|
|||||||
18
helpers/array.js
Normal file
18
helpers/array.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export function getNextSmaller(array, pivot) {
|
||||||
|
if (!array || !Array.isArray(array) || !array.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return array
|
||||||
|
.sort()
|
||||||
|
.reverse()
|
||||||
|
.find((item) => item < pivot)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextBigger(array, pivot) {
|
||||||
|
if (!array || !Array.isArray(array) || !array.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.sort().find((day) => day > pivot)
|
||||||
|
}
|
||||||
13
helpers/array.test.js
Normal file
13
helpers/array.test.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { getNextSmaller, getNextBigger } from './array'
|
||||||
|
|
||||||
|
test('getPreviousInOrder', () => {
|
||||||
|
const result = getNextSmaller([3, 1, 2, 0, 8, 9, 10], 5)
|
||||||
|
|
||||||
|
expect(result).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getNextInOrder', () => {
|
||||||
|
const result = getNextBigger([7, 8, 9, 3, 1, 2], 4)
|
||||||
|
|
||||||
|
expect(result).toEqual(7)
|
||||||
|
})
|
||||||
4287
package-lock.json
generated
4287
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -5,7 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:ci": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.5.0",
|
||||||
@@ -14,13 +16,15 @@
|
|||||||
"next": "^9.5.0",
|
"next": "^9.5.0",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-bootstrap": "^1.3.0",
|
"react-bootstrap": "^1.3.0",
|
||||||
"react-dates": "^21.8.0",
|
"react-day-picker": "^7.4.8",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"rebass": "^4.0.7",
|
"rebass": "^4.0.7",
|
||||||
"swr": "^0.2.3"
|
"swr": "^0.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"babel-jest": "^26.2.2",
|
||||||
"eslint": "^7.5.0",
|
"eslint": "^7.5.0",
|
||||||
"eslint-plugin-react": "^7.20.5"
|
"eslint-plugin-react": "^7.20.5",
|
||||||
|
"jest": "^26.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import 'react-dates/lib/css/_datepicker.css'
|
import 'react-day-picker/lib/style.css'
|
||||||
|
|
||||||
import 'moment'
|
import 'moment'
|
||||||
import 'moment/locale/de'
|
import 'moment/locale/de'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
import Wizard from '../components/wizard'
|
import Wizard from '../components/wizard'
|
||||||
@@ -22,8 +24,6 @@ export default function Home() {
|
|||||||
<a>Freundeskreis des VCP Rosenfeld</a>
|
<a>Freundeskreis des VCP Rosenfeld</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style jsx>{``}</style>
|
|
||||||
|
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@@ -37,6 +37,27 @@ export default function Home() {
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Selectable
|
||||||
|
.DayPicker-Day--selected:not(.DayPicker-Day--start):not(.DayPicker-Day--end):not(.DayPicker-Day--outside) {
|
||||||
|
background-color: #f0f8ff !important;
|
||||||
|
color: #4a90e2;
|
||||||
|
}
|
||||||
|
.Selectable .DayPicker-Day {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.Selectable .DayPicker-Day--start {
|
||||||
|
border-top-left-radius: 50% !important;
|
||||||
|
border-bottom-left-radius: 50% !important;
|
||||||
|
}
|
||||||
|
.Selectable .DayPicker-Day--end {
|
||||||
|
border-top-right-radius: 50% !important;
|
||||||
|
border-bottom-right-radius: 50% !important;
|
||||||
|
}
|
||||||
|
.Selectable .DayPicker-Day--dayBooked:not(.DayPicker-Day--outside) {
|
||||||
|
color: #505050;
|
||||||
|
background-color: #fcc;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user