diff --git a/package.json b/package.json index 1136566..bb53770 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react": "^16.13.1", "react-app-rewired": "^2.1.6", "react-dom": "^16.13.1", + "react-helmet": "^6.0.0", "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", "rsuite": "^4.5.0" diff --git a/src/components/General/TimezonePicker.js b/src/components/General/TimezonePicker.js index 3b558ca..8ed1d87 100644 --- a/src/components/General/TimezonePicker.js +++ b/src/components/General/TimezonePicker.js @@ -9,10 +9,10 @@ export default function TimezonePicker() { setTimezone(item.timezone)} onClean={(event) => setTimezone('')} value={timezone} diff --git a/src/components/General/Title.js b/src/components/General/Title.js new file mode 100644 index 0000000..dc6b261 --- /dev/null +++ b/src/components/General/Title.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { Helmet } from 'react-helmet'; + +export default function TitleComponent({ title }) { + var defaultTitle = 'Meeting Planner'; + return ( + + {title ? `${title} | Meeting Planner` : defaultTitle} + + ); +} diff --git a/src/components/Navbar/NavBar.js b/src/components/Navbar/NavBar.js index 3fd68cf..23b70e0 100644 --- a/src/components/Navbar/NavBar.js +++ b/src/components/Navbar/NavBar.js @@ -1,17 +1,28 @@ import React from 'react'; -import { Header, Navbar } from 'rsuite'; +import { Header, Navbar, Nav } from 'rsuite'; import MenuDropdown from './MenuDropdown'; +import Title from './../General/Title'; -const headerStyle = { - borderRadius: '7px 7px 0 0', -}; - -export default function NavBar() { +export default function NavBar({ title }) { return (
- - + + <Navbar appearance='inverse' > + <Navbar.Body + style={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }} + > + <Nav style={{ visibility: 'hidden' }}> + {/* This hidden nav is a hack to have the title perfectly centered. */} + <MenuDropdown /> + </Nav> + <div> + <h3>{title}</h3> + </div> <MenuDropdown /> </Navbar.Body> </Navbar> diff --git a/src/components/Schedule/DaySelector.js b/src/components/Schedule/DaySelector.js index b95a066..8f81eaa 100644 --- a/src/components/Schedule/DaySelector.js +++ b/src/components/Schedule/DaySelector.js @@ -3,13 +3,17 @@ import FullCalendar from '@fullcalendar/react'; import dayGridPlugin from '@fullcalendar/daygrid'; import interaction from '@fullcalendar/interaction'; +import './DaySelector.less'; + export default function DaySelector({ eventsList, handleSelect, handleClear }) { return ( <FullCalendar - initialView='dayGridMonth' plugins={[dayGridPlugin, interaction]} + initialView='dayGridMonth' + // showNonCurrentDates={false} selectable={true} - unselectAuto={false} + // unselectAuto={false} + longPressDelay={150} select={(info) => handleSelect(info)} defaultAllDay={true} events={eventsList} diff --git a/src/components/Schedule/DaySelector.less b/src/components/Schedule/DaySelector.less new file mode 100644 index 0000000..492a2e6 --- /dev/null +++ b/src/components/Schedule/DaySelector.less @@ -0,0 +1,3 @@ +.fc-view-harness { + padding-bottom: 100% !important +} \ No newline at end of file diff --git a/src/components/Schedule/SelectedDates.js b/src/components/Schedule/SelectedDates.js index d917345..10f153b 100644 --- a/src/components/Schedule/SelectedDates.js +++ b/src/components/Schedule/SelectedDates.js @@ -5,7 +5,6 @@ import { Divider, Icon, IconButton } from 'rsuite'; export default function SelectedDates({ datesList, handleDelete }) { return ( <> - <h3>Dates selected</h3> <ul> {datesList.map((date) => ( <li key={date}> diff --git a/src/screens/App.js b/src/screens/App.js index b71d78d..c3e4089 100644 --- a/src/screens/App.js +++ b/src/screens/App.js @@ -3,27 +3,33 @@ import 'rsuite/lib/styles/index.less'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import Dashboard from './Dashboard'; -import Schedule from './Schedule'; +import Schedule from './Schedule/Schedule'; import Login from './Login'; import Register from './Register'; -import NavBar from '../components/Navbar/NavBar'; + +const titles = { + schedule: 'Schedule a meeting', + dashboard: 'Dashboard', + login: 'Login', + register: 'Registration', + settings: 'Settings' +}; export default function App() { return ( <Router> - <NavBar /> <Switch> <Route path='/' exact> - <Schedule /> + <Schedule title={titles.schedule} /> </Route> <Route path='/dashboard'> - <Dashboard /> + <Dashboard title={titles.dashboard}/> </Route> <Route path='/login'> - <Login /> + <Login title={titles.login}/> </Route> <Route path='/register'> - <Register /> + <Register title={titles.register} /> </Route> </Switch> </Router> diff --git a/src/screens/Login.js b/src/screens/Login.js index 7153d55..d019808 100644 --- a/src/screens/Login.js +++ b/src/screens/Login.js @@ -1,14 +1,10 @@ import React from 'react'; -import { - FlexboxGrid, - Form, - FormGroup, - FormControl, - // HelpBlock, - Button, -} from 'rsuite'; +import { Panel, Form, FormGroup, FormControl, ControlLabel, HelpBlock, Button } from 'rsuite'; +import NavBar from './../components/Navbar/NavBar'; + +// TODO Move to a .less file const boxStyle = { maxWidth: 373, margin: '0 auto', @@ -19,37 +15,34 @@ const boxStyle = { padding: '1rem', }; -const h3Style = { margin: '1rem' }; - -export default function Login() { +export default function Login({ title }) { return ( - <FlexboxGrid justify='center' style={boxStyle}> - <h3 style={h3Style}>Login</h3> - <Form horizontal> - <FormGroup> - <FormControl - name='email' - type='email' - placeholder='email' - /> - </FormGroup> - <FormGroup> - <FormControl - name='password' - type='password' - placeholder='password' - /> - {/* <HelpBlock tooltip> - Minimum password length is 8 characters - </HelpBlock> */} - </FormGroup> - <FormGroup> - <Button appearance='primary' block> - Sign in - </Button> - </FormGroup> - <Button appearance='link'>Forgot password?</Button> - </Form> - </FlexboxGrid> + <> + <NavBar title={title} /> + <Panel bordered style={boxStyle}> + <Form> + <FormGroup> + <ControlLabel>Email</ControlLabel> + <FormControl + name='email' + type='email' + /> + </FormGroup> + <ControlLabel>Password</ControlLabel> + <FormGroup> + <FormControl + name='password' + type='password' + /> + </FormGroup> + <FormGroup> + <Button appearance='primary' block size="lg"> + Sign in + </Button> + </FormGroup> + <Button appearance='link'>Forgot password?</Button> + </Form> + </Panel> + </> ); } diff --git a/src/screens/Register.js b/src/screens/Register.js index 608feb3..820fa85 100644 --- a/src/screens/Register.js +++ b/src/screens/Register.js @@ -1,16 +1,11 @@ import React from 'react'; -import { - FlexboxGrid, - Form, - FormGroup, - FormControl, - HelpBlock, - Button, -} from 'rsuite'; +import { Form, FormGroup, FormControl, ControlLabel, HelpBlock, Button, Panel } from 'rsuite'; +import NavBar from './../components/Navbar/NavBar'; import TimezonePicker from '../components/General/TimezonePicker'; +// TODO Move to a .less file const boxStyle = { maxWidth: 373, margin: '0 auto', @@ -21,46 +16,47 @@ const boxStyle = { padding: '1rem', }; -const h3Style = { margin: '1rem' }; - -export default function Register() { +export default function Register({ title }) { return ( - <FlexboxGrid justify='center' style={boxStyle}> - <h3 style={h3Style}>Register</h3> - <Form horizontal> - <FormGroup> - <FormControl - name='username' - type='text' - placeholder='username' - /> - </FormGroup> - <FormGroup> - <FormControl - name='email' - type='email' - placeholder='email' - /> - </FormGroup> - <FormGroup> - <TimezonePicker /> - </FormGroup> - <FormGroup> - <FormControl - name='password' - type='password' - placeholder='password' - /> - <HelpBlock tooltip> - Minimum password length is 8 characters - </HelpBlock> - </FormGroup> - <FormGroup> - <Button appearance='primary' block> - Register - </Button> - </FormGroup> - </Form> - </FlexboxGrid> + <> + <NavBar title={title} /> + <Panel bordered style={boxStyle}> + <Form> + <FormGroup> + <ControlLabel>Username</ControlLabel> + <FormControl + name='username' + type='text' + /> + </FormGroup> + <FormGroup> + <ControlLabel>Email</ControlLabel> + <FormControl + name='email' + type='email' + /> + </FormGroup> + <ControlLabel>Timezone</ControlLabel> + <FormGroup> + <TimezonePicker /> + </FormGroup> + <ControlLabel>Password</ControlLabel> + <FormGroup> + <FormControl + name='password' + type='password' + /> + <HelpBlock> + Minimum password length is 8 characters + </HelpBlock> + </FormGroup> + <FormGroup> + <Button appearance='primary' block size="lg"> + Register + </Button> + </FormGroup> + </Form> + </Panel> + </> ); } diff --git a/src/screens/Schedule.js b/src/screens/Schedule/Schedule.js similarity index 55% rename from src/screens/Schedule.js rename to src/screens/Schedule/Schedule.js index bb5cff1..d25709d 100644 --- a/src/screens/Schedule.js +++ b/src/screens/Schedule/Schedule.js @@ -1,18 +1,31 @@ import React, { useState, useEffect } from 'react'; -import { Container, Form, FormControl, FormGroup, Input, Button } from 'rsuite'; +import { + Panel, + Form, + FormControl, + ControlLabel, + Input, + HelpBlock, + Button, + ButtonGroup, + Message, + FormGroup, +} from 'rsuite'; import { DateTime } from 'luxon'; -import TimezonePicker from '../components/General/TimezonePicker'; -import DaySelector from '../components/Schedule/DaySelector'; -import DurationSelector from '../components/Schedule/DurationSelector'; -import SelectedDates from '../components/Schedule/SelectedDates'; +import NavBar from '../../components/Navbar/NavBar'; +import TimezonePicker from '../../components/General/TimezonePicker'; +import DaySelector from '../../components/Schedule/DaySelector'; +import DurationSelector from '../../components/Schedule/DurationSelector'; +import SelectedDates from '../../components/Schedule/SelectedDates'; -export default function Schedule() { +import './Schedule.less'; + +export default function Schedule({ title }) { const [eventsList, setEventsList] = useState([]); const [datesList, setDatesList] = useState(eventsToDates(eventsList)); useEffect(() => { - // Update selected dates let updatedDates = eventsToDates(eventsList).sort(); setDatesList(updatedDates); }, [eventsList]); @@ -104,45 +117,80 @@ export default function Schedule() { }; return ( - <Container> - <h3>Schedule a meeting</h3> - <Form> - <FormGroup> - <FormControl name='title' type='text' placeholder='title' /> - </FormGroup> - <FormGroup> - <Input - name='description' - componentClass='textarea' - type='text' - rows={3} - placeholder='Description' - /> - </FormGroup> - <FormGroup> - <TimezonePicker /> - </FormGroup> - <FormGroup style={{ width: 200 }}> - <DurationSelector /> - </FormGroup> - <FormGroup> - <DaySelector - eventsList={eventsList} - handleSelect={handleSelect} - handleClear={handleClear} - /> - </FormGroup> - {datesList.length > 0 && ( - <FormGroup> - <SelectedDates - datesList={datesList} - handleDelete={handleDelete} + <> + <NavBar title={title} /> + <Panel style={containerStyle}> + <Form className={'meeting-container'}> + <div className={'meeting-info'}> + <FormGroup> + <ControlLabel>Title</ControlLabel> + <FormControl name='title' type='text' /> + <HelpBlock>This field is required</HelpBlock> + </FormGroup> + <FormGroup> + <ControlLabel>Description</ControlLabel> + <Input + name='description' + componentClass='textarea' + type='text' + rows={3} + placeholder='(optional)' + /> + </FormGroup> + <div className='meeting-options-inline'> + <FormGroup className='meeting-timezone'> + <ControlLabel>Timezone</ControlLabel> + <TimezonePicker /> + </FormGroup> + <FormGroup className='meeting-duration'> + <ControlLabel>Duration</ControlLabel> + <DurationSelector /> + </FormGroup> + </div> + <ButtonGroup justified> + <Button + appearance='ghost' + block + size='lg' + disabled={datesList.length === 0} + onClick={() => handleClear()} + > + Clear selection + </Button> + <Button + appearance='primary' + size='lg' + block + disabled={datesList.length === 0} + > + Confirm dates + </Button> + </ButtonGroup> + <div className={'selected-dates'}></div> + {datesList.length > 0 && ( + <> + <SelectedDates + datesList={datesList} + handleDelete={handleDelete} + /> + </> + )} + </div> + <div className={'day-selector'}> + <Message + showIcon + type='info' + description='Select possible meetings dates on the calendar.' /> - <Button>Confirm dates</Button> - </FormGroup> - )} - </Form> - </Container> + <DaySelector + eventsList={eventsList} + handleSelect={handleSelect} + handleClear={handleClear} + /> + </div> + </Form> + </Panel> + </> ); } @@ -154,3 +202,10 @@ const eventsToDates = (events) => { }); return dates; }; + +const containerStyle = { + // TODO Move to a .less file + maxWidth: '1200px', + margin: '30px auto', + backgroundColor: 'rgba(255,255,255,0.6)', +}; diff --git a/src/screens/Schedule/Schedule.less b/src/screens/Schedule/Schedule.less new file mode 100644 index 0000000..f7a4577 --- /dev/null +++ b/src/screens/Schedule/Schedule.less @@ -0,0 +1,46 @@ +.meeting-container { + display: grid; + grid-template-columns: 6fr 5fr; + grid-template-rows: 1fr; + grid-template-areas: + 'selector info'; + column-gap: 2em; + row-gap: 2em; + grid-column-gap: 2em; + grid-row-gap: 2em; +} + +.meeting-info { + grid-area: info; + margin: 1em 0 3em 0; +} + +.day-selector { + grid-area: selector; + margin: 1em 0 3em 0; +} + +.selected-dates { + grid-area: selected; + ul { + column-count: 2; + } +} + +.meeting-options-inline { + display: flex; + justify-content: space-between; + + .meeting-timezone { + width: 65% + } + + .meeting-duration { + width:30% + } +} + +// Fix button alignment bug +.rs-btn-block + .rs-btn-block { + margin-top: unset; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a16c4cf..8fd6bcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8795,6 +8795,21 @@ react-error-overlay@^6.0.7: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108" integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA== +react-fast-compare@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-helmet@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.0.0.tgz#fcb93ebaca3ba562a686eb2f1f9d46093d83b5f8" + integrity sha512-My6S4sa0uHN/IuVUn0HFmasW5xj9clTkB9qmMngscVycQ5vVG51Qp44BEvLJ4lixupTwDlU9qX1/sCrMN4AEPg== + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^2.0.4" + react-side-effect "^2.1.0" + react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -8894,6 +8909,11 @@ react-scripts@3.4.1: optionalDependencies: fsevents "2.1.2" +react-side-effect@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3" + integrity sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg== + react-virtualized@^9.21.0: version "9.21.2" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e"