@reach/router#navigate TypeScript Examples
The following examples show how to use
@reach/router#navigate.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: PageNotFound.tsx From office-booker with MIT License | 6 votes |
PageNotFound: React.FC<RouteComponentProps> = () => {
// Local state
const [activateRedirect, setActivateRedirect] = useState(false);
// Effects
useEffect(() => {
setTimeout(() => {
setActivateRedirect(true);
}, 5000);
}, []);
// Render
return (
<Layout>
<ErrorPageStyles>
<h2>Page Not Found</h2>
<p>We'll redirect you to home in 5 seconds</p>
{activateRedirect && <Redirect to="/" noThrow />}
<OurButton
type="submit"
variant="contained"
color="primary"
onClick={() => {
navigate(`/`);
}}
>
Redirect Now
</OurButton>
</ErrorPageStyles>
</Layout>
);
}
Example #2
Source File: Header.tsx From office-booker with MIT License | 6 votes |
Header: React.FC = () => (
<HeaderStyles>
<div className="app-icon">
<Link component="button" onClick={() => navigate('/')}>
<img src={OfficesIcon} alt="Office Logo" width={30} height={30} />
</Link>
</div>
<div className="title">
<h1>
<Link href="/">Office Booker</Link>
</h1>
</div>
<div className="lab-logo">
<Link href="https://github.com/o2Labs/" rel="noopener noreferrer" target="_blank">
<img src={LabIcon} alt="The Lab Logo" width={32} height={32} />
</Link>
</div>
</HeaderStyles>
)
Example #3
Source File: header.tsx From nyxo-website with MIT License | 5 votes |
signOut = () => {
Auth.signOut()
.then(() => navigate("/me/login"))
.catch((err) => console.log(err))
}
Example #4
Source File: NextBooking.tsx From office-booker with MIT License | 5 votes |
NextBooking: React.FC<Props> = (props) => {
// Local state
const [todaysBooking, setTodaysBooking] = useState<Booking | undefined>();
// Effects
useEffect(() => {
if (props.bookings.length > 0) {
// Find a booking for today
setTodaysBooking(
props.bookings.find((b) => isToday(parse(b.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS)))
);
}
}, [props.bookings]);
// Render
if (!todaysBooking) {
return null;
}
return (
<NextBookingStyles>
<h2>Today's Booking</h2>
<h3>
{format(
parse(todaysBooking.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
'do LLL',
DATE_FNS_OPTIONS
)}{' '}
<span>@</span> {todaysBooking.office.name}
</h3>
<OurButton
type="submit"
variant="contained"
color="primary"
onClick={() => {
navigate(`./booking/${todaysBooking?.id}`);
}}
>
View pass
</OurButton>
<div className="upcoming-header">
<Link component="button" color="inherit" onClick={() => navigate('/bookings')}>
View upcoming bookings
</Link>
</div>
</NextBookingStyles>
);
}
Example #5
Source File: Home.tsx From listo with MIT License | 5 votes |
Home = (props: HomeProps) => {
const isSlack = props.listoMeta
? Boolean(props.listoMeta.slackChannelLink && props.listoMeta.slackChannel)
: false;
const isTrelloBoard = props.listoMeta
? Boolean(props.listoMeta.exampleTrelloBoardLink)
: false;
return (
<React.Fragment>
<Grid container spacing={5}>
<Grid item xs={12}>
<Typography variant="h4" gutterBottom>
Welcome to Listo
</Typography>
</Grid>
</Grid>
<Grid container spacing={5}>
<Grid item xs={12}>
<Typography variant="body1" gutterBottom>
This tool provides advice on security, reliability and architecture
requirements when developing products. Think of it as insight into
the collective technical knowledge of an engineering community.
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1" gutterBottom>
At the end of the review we’ll provide a link to a Trello board
which contains the checklists for your project.{' '}
{isTrelloBoard && (
<a
href={props.listoMeta.exampleTrelloBoardLink}
target="_blank"
rel="noopener noreferrer"
>
Here is an example of a full Trello board.
</a>
)}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1" gutterBottom>
Got questions? <a href={`/faq`}>Check out the FAQ's</a> or message
us on Slack{' '}
{isSlack && (
<a
href={props.listoMeta.slackChannelLink}
target="_blank"
rel="noopener noreferrer"
>
{props.listoMeta.slackChannel}
</a>
)}
</Typography>
</Grid>
<Grid item xs={12} justify="center">
<Button
variant="contained"
color="primary"
onClick={() => navigate('/assessment')}
>
Do self assessment
</Button>
</Grid>
</Grid>
</React.Fragment>
);
}
Example #6
Source File: activate.tsx From midway with MIT License | 4 votes |
Activate = (props: { id: string; token: string }) => {
const [passwordField1, setPasswordField1] = useState("")
const [passwordField2, setPasswordField2] = useState("")
const [attempts, setAttempts] = useState(0)
const verifyAccount = useCallback(
async e => {
setAttempts(attempts + 1)
if (passwordField1 !== passwordField2) {
await Timeout.set(400)
throw new Error("Passwords do not match.")
}
if (!PasswordSchema.validate(passwordField1)) {
throw new Error(
"Your password should be between 8 and 100 characters, and have at least one lowercase and one uppercase letter."
)
}
const body = {
id: encode("Customer", props.id),
input: {
activationToken: props.token,
password: passwordField1,
},
}
const res = await fetch(`/.netlify/functions/activate`, {
method: "POST",
body: JSON.stringify(body),
})
try {
const customer = await res.json()
if (customer.error) {
const err = new Error("Server Error: Account verification fetch response error.")
throw err
} else if (!res.ok) {
const err = new Error("Server Error: Account verification fetch not OK.")
throw err
}
await Timeout.set(400)
await navigate("/account/login")
} catch (err) {
throw err
}
// hacky way to get it to run anew every time
// and maybe in the future redirect them if they
// need help.
},
[passwordField1, passwordField2, attempts]
)
const { error, load, isReloading, isPending } = useDeferredLoads(
"verifyAccount",
verifyAccount,
{}
)
return (
<div>
<Helmet title="activate account" />
<div className="nav-spacer" />
<div className="accounts__wrapper f col aic jcc y px1">
<div className="x">
<div className="container--xl ma ac">
<h5 className="pb0 caps sans ls">Activate Account</h5>
<h2 className="my0">Almost there.</h2>
</div>
<div className="x container--s al mxa">
{error && !isReloading && (
<div className="studio mt1 error f col">
<span>{error.message}</span>
</div>
)}
{(isPending || isReloading) && (
<div className="f jcc p1">
<span>Loading</span>
</div>
)}
<div
className={cx(
"x container--s col mya",
isPending || isReloading ? "invisible" : "visible"
)}
>
<div className="pb1 x pya">
<div className="caps sans ls my1">Password</div>
<input
className="py1 px1 x s16 s16"
type="password"
value={passwordField1}
onChange={e => setPasswordField1(e.target.value)}
placeholder="Password"
/>
</div>
<div className="pb1 mb1 x pya">
<div className="caps sans ls mt1 py05">Confirm Password</div>
<input
className="py1 px1 x mb1 s16"
type="password"
value={passwordField2}
onChange={e => setPasswordField2(e.target.value)}
placeholder="Confirm Password"
/>
</div>
</div>
<div className="ac x">
<button
type="submit"
className="button button--wide cg ac akz ls-s mt1 inline-block caps s14"
onClick={load}
>
Activate
</button>
<div className="container--m mya pt1 aic">
<p className=" sans s14 pt1 ac">
Already have an account?{" "}
<Link className="underline active" to="/account/login">
Login
</Link>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
Example #7
Source File: Bookings.tsx From office-booker with MIT License | 4 votes |
Bookings: React.FC<RouteComponentProps> = () => {
// Global state
const { state, dispatch } = useContext(AppContext);
const { user } = state;
// Local state
const [loading, setLoading] = useState(true);
const [offices, setOffices] = useState<Office[] | undefined>();
const [selectedOffice, setSelectedOffice] = useState<Office | undefined>();
const [selectedDate, setSelectedDate] = useState(new Date());
const [dbBookings, setDbBookings] = useState<Booking[] | undefined>();
const [sortedBookings, setSortedBookings] = useState<Booking[] | undefined>();
const [sortBy, setSortBy] = useState<keyof Booking>('user');
const [sortOrder, setSortOrder] = useState<SortOrder>('asc');
const [bookingToCancel, setBookingToCancel] = useState<undefined | Booking>();
// Theme
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
// Helpers
const getAllBookings = useCallback(() => {
if (selectedOffice) {
getBookings({ office: selectedOffice, date: format(selectedDate, 'yyyy-MM-dd') })
.then((data) => setDbBookings(data))
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [selectedOffice, selectedDate, dispatch]);
const findOffice = useCallback(
(name: OfficeWithSlots['name']) => offices && offices.find((o) => o.name === name),
[offices]
);
// Effects
useEffect(() => {
if (user) {
// Get all offices admin can manage
getOffices()
.then((data) =>
setOffices(
data.filter((office) =>
user.permissions.officesCanManageBookingsFor.find(
(userOffice) => userOffice.name === office.name
)
)
)
)
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [user, dispatch]);
useEffect(() => {
if (user && offices && offices.length > 0 && !selectedOffice) {
// Retrieve first office user can manage bookings for
setSelectedOffice(user.permissions.officesCanManageBookingsFor[0]);
}
}, [user, offices, selectedOffice, findOffice]);
useEffect(() => {
if (selectedOffice) {
// Retrieve bookings
getAllBookings();
}
}, [dispatch, selectedOffice, selectedDate, getAllBookings]);
useEffect(() => {
if (dbBookings) {
// Sort it!
setSortedBookings(sortData([...dbBookings], sortBy, sortOrder));
}
}, [dbBookings, sortBy, sortOrder]);
useEffect(() => {
if (loading && sortedBookings) {
// Wait for local state to be ready
setLoading(false);
}
}, [loading, sortedBookings]);
// Handlers
const handleSort = (key: keyof Booking) => {
if (key === sortBy) {
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
} else {
setSortBy(key);
}
};
const handleCancelBooking = (booking: Booking) => {
cancelBooking(booking.id, booking.user)
.then(() => {
// Clear selected booking
setBookingToCancel(undefined);
// Retrieve updated bookings
getAllBookings();
// Show confirmation alert
dispatch({
type: 'SET_ALERT',
payload: {
message: 'Booking cancelled',
color: 'success',
},
});
})
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
};
// Render
if (!user) {
return null;
}
return (
<AdminLayout currentRoute="bookings">
<BookingStyles>
{loading ? (
<Loading />
) : (
<>
<h3>Bookings</h3>
<OurButton
startIcon={<AddCircleIcon />}
type="submit"
color="secondary"
onClick={() => navigate('/admin/booking')}
variant="contained"
size="small"
>
New Booking
</OurButton>
{selectedOffice && (
<Paper square className="table-container">
<div className="filter">
<FormControl className="filter-office">
<InputLabel id="office-label">Office</InputLabel>
<Select
labelId="office-label"
id="office"
value={selectedOffice.name}
onChange={(e) => setSelectedOffice(findOffice(e.target.value as string))}
>
{user.permissions.officesCanManageBookingsFor.map((office, index) => (
<MenuItem value={office.name} key={index}>
{office.name}
</MenuItem>
))}
</Select>
</FormControl>
<div className="filter-date">
<IconButton
onClick={() => setSelectedDate(addDays(new Date(selectedDate), -1))}
className="date-arrow"
>
<KeyboardArrowLeftIcon />
</IconButton>
<DatePicker
autoOk
disableToolbar
variant="inline"
label="Date"
format="dd/MM/yyyy"
value={selectedDate}
onChange={(date) => setSelectedDate(date as Date)}
className="date-picker"
/>
<IconButton
onClick={() => setSelectedDate(addDays(new Date(selectedDate), 1))}
className="date-arrow"
>
<KeyboardArrowRightIcon />
</IconButton>
</div>
</div>
<div className="total-bookings">
<InputLabel className="bookings-count-label">Bookings:</InputLabel>
<span>{sortedBookings && sortedBookings.length}</span>
</div>
<TableContainer className="table">
<Table>
<TableHead>
<TableRow>
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'user'}
direction={sortOrder}
onClick={() => handleSort('user')}
>
User
</TableSortLabel>
</TableCell>
{selectedOffice.parkingQuota > 0 && (
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'parking'}
direction={sortOrder}
onClick={() => handleSort('parking')}
>
Parking
</TableSortLabel>
</TableCell>
)}
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{sortedBookings && sortedBookings.length > 0 ? (
sortedBookings.map((booking, index) => {
const parsedDate = parseISO(booking.date);
return (
<TableRow key={index}>
<TableCell>{booking.user}</TableCell>
{selectedOffice.parkingQuota > 0 && (
<TableCell>{booking.parking ? 'Yes' : 'No'}</TableCell>
)}
{isToday(parsedDate) || !isPast(parsedDate) ? (
<TableCell align="right">
<div className="btn-container">
<OurButton
type="submit"
variant="contained"
color="secondary"
size="small"
onClick={() => setBookingToCancel(booking)}
>
Cancel
</OurButton>
</div>
</TableCell>
) : (
<TableCell />
)}
</TableRow>
);
})
) : (
<TableRow>
<TableCell>No bookings found</TableCell>
{selectedOffice.parkingQuota > 0 && <TableCell />}
<TableCell />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Paper>
)}
</>
)}
{bookingToCancel && (
<Dialog fullScreen={fullScreen} open={true} onClose={() => setBookingToCancel(undefined)}>
<DialogTitle>{'Are you sure you want to cancel this booking?'}</DialogTitle>
<DialogContent>
<DialogContentText>
Booking for <strong>{bookingToCancel.user}</strong> on{' '}
<strong>{format(parseISO(bookingToCancel.date), 'do LLLL')}</strong> for{' '}
<strong>{bookingToCancel.office.name}</strong>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setBookingToCancel(undefined)} color="primary" autoFocus>
No
</Button>
<Button
autoFocus
onClick={() => handleCancelBooking(bookingToCancel)}
color="primary"
>
Yes
</Button>
</DialogActions>
</Dialog>
)}
</BookingStyles>
</AdminLayout>
);
}
Example #8
Source File: User.tsx From office-booker with MIT License | 4 votes |
UserAdmin: React.FC<RouteComponentProps<{ email: string }>> = (props) => {
// Global state
const { state, dispatch } = useContext(AppContext);
const { config, user } = state;
const canEdit = user?.permissions.canEditUsers === true;
// Local state
const [loading, setLoading] = useState(true);
const [offices, setOffices] = useState<Office[] | undefined>();
const [selectedUser, setSelectedUser] = useState<User | undefined>();
const [autoApprovedSelected, setAutoApprovedSelected] = useState(false);
// Effects
useEffect(() => {
if (user && !user.permissions.canViewUsers) {
// No permissions - Bounce to home page
navigate('/');
}
}, [user]);
useEffect(() => {
if (user) {
// Get selected user
getUser(props.email || '')
.then((selectedUser) => setSelectedUser(selectedUser))
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [user, props.email, dispatch]);
useEffect(() => {
if (user && selectedUser) {
// Get all offices admin can manage
setOffices(user.permissions.officesCanManageBookingsFor);
if (config?.reasonToBookRequired && selectedUser.autoApproved !== undefined) {
setAutoApprovedSelected(selectedUser.autoApproved);
}
}
}, [user, selectedUser, dispatch, config?.reasonToBookRequired]);
useEffect(() => {
if (offices) {
// Wait for global state to be ready
setLoading(false);
}
}, [offices]);
// Handlers
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Validate
if (selectedUser === undefined) {
return;
}
if (selectedUser.role.name === 'Office Admin' && selectedUser.role.offices.length === 0) {
return dispatch({
type: 'SET_ALERT',
payload: {
message: 'Please select at least one office',
color: 'error',
},
});
}
// Create/update user
const role = selectedUser.role.name === 'System Admin' ? undefined : selectedUser.role;
const putBody = config?.reasonToBookRequired
? {
email: selectedUser.email,
quota: selectedUser.quota,
role,
autoApproved: selectedUser.autoApproved,
}
: {
email: selectedUser.email,
quota: selectedUser.quota,
role,
};
putUser(putBody)
.then((updatedUser) => {
// Update local state
setSelectedUser(updatedUser);
// Confirmation alert
dispatch({
type: 'SET_ALERT',
payload: {
message: `${updatedUser.email} updated`,
color: 'success',
},
});
})
.catch((err) => {
// Handle errors
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
};
// Render
if (!user) {
return null;
}
return (
<AdminLayout currentRoute="users">
<UserStyles>
{loading || !selectedUser || !offices ? (
<Loading />
) : (
<>
<h3>Users</h3>
<Paper square className="form-container">
<h4>Edit user</h4>
<h5>{selectedUser.email}</h5>
<form onSubmit={handleFormSubmit}>
<div className="field">
<FormControl variant="outlined" className="input">
<InputLabel id="role-label" shrink>
Role
</InputLabel>
<Select
labelId="role-label"
id="role"
value={selectedUser.role.name}
disabled={selectedUser.role.name === 'System Admin' || !canEdit}
onChange={(e) => {
const { value } = e.target;
setSelectedUser((selectedUser) => {
if (selectedUser === undefined) {
return;
}
if (value === 'Default') {
return { ...selectedUser, role: { name: 'Default' } };
}
if (value === 'Office Admin') {
return {
...selectedUser,
role: { name: 'Office Admin', offices: [] },
};
}
return selectedUser;
});
}}
label="Role"
>
<MenuItem value={'Default'}>Default</MenuItem>
<MenuItem value={'Office Admin'}>Office Admin</MenuItem>
<MenuItem value={'System Admin'} disabled>
System Admin
</MenuItem>
</Select>
</FormControl>
</div>
{selectedUser.role.name === 'Office Admin' && (
<div className="field">
<Autocomplete
multiple
disabled={!canEdit}
options={offices.map((o) => o.name)}
value={
selectedUser.role.name === 'Office Admin'
? selectedUser.role.offices.map((office) => office.name)
: []
}
onChange={(_e, value) =>
setSelectedUser((selectedUser) => {
if (selectedUser === undefined) {
return;
}
return {
...selectedUser,
role: {
name: 'Office Admin',
offices: offices.filter((office) => value.includes(office.name)),
},
};
})
}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label="Offices"
fullWidth={false}
className="input"
/>
)}
renderTags={(selectedOffices, tagProps) =>
selectedOffices.map((office, index: number) => (
<Chip
variant="outlined"
key={index}
label={office}
{...tagProps({ index })}
/>
))
}
/>
</div>
)}
<div className="field">
<TextField
type="number"
variant="outlined"
disabled={!canEdit}
label="Weekly quota"
value={selectedUser.quota}
onChange={(e) => {
// Between 0 and 7
const quota = Number.parseInt(e.target.value);
setSelectedUser(
(selectedUser) =>
selectedUser && {
...selectedUser,
quota: quota >= 0 && quota <= 7 ? quota : quota > 7 ? 7 : 0,
}
);
}}
className="input"
/>
</div>
{config?.reasonToBookRequired && (
<div className="field">
<FormControlLabel
control={
<Checkbox
checked={autoApprovedSelected}
onChange={(event) =>
setSelectedUser(
(selectedUser) =>
selectedUser && {
...selectedUser,
autoApproved: event.target.checked,
}
)
}
name="checkedB"
color="primary"
/>
}
label="Auto Approved"
/>
</div>
)}
{canEdit && (
<div className="buttons">
<OurButton
type="submit"
color="primary"
variant="contained"
disabled={selectedUser.quota < 0}
>
Save
</OurButton>
</div>
)}
</form>
</Paper>
<section className="help">
<h3>About Roles</h3>
<h4>Default</h4>
<ul>
<li>Any user with a valid email address gets this role.</li>
<li>Can manage their own bookings only.</li>
</ul>
<h4>System Admin</h4>
<ul>
<li>Must be configured in infrastructure.</li>
<li>Can view and edit all bookings in the system.</li>
<li>Can view and edit all users</li>
</ul>
<h4>Office Admin</h4>
<ul>
<li>Must be assigned by a System Admin.</li>
<li>Can view and edit bookings for their assigned offices.</li>
<li>Can view other users (but can't edit).</li>
</ul>
<p>A default quota is applied to all users regardless of role.</p>
</section>
</>
)}
</UserStyles>
</AdminLayout>
);
}
Example #9
Source File: UserBookings.tsx From office-booker with MIT License | 4 votes |
UserBookings: React.FC<RouteComponentProps<{ email: string }>> = (props) => {
// Global state
const { state, dispatch } = useContext(AppContext);
const { user } = state;
// Local state
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<User | undefined>();
const [bookings, setBookings] = useState<Booking[] | undefined>();
const [bookingToCancel, setBookingToCancel] = useState<undefined | Booking>();
const [sortedBookings, setSortedBookings] = useState<Booking[] | undefined>();
const [sortBy, setSortBy] = useState<keyof Booking>('date');
const [sortOrder, setSortOrder] = useState<SortOrder>('asc');
// Theme
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
// Effects
useEffect(() => {
if (props.email) {
getBookings({ user: props.email })
.then((data) => {
// Split for previous and upcoming
setBookings(data);
})
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [props.email, dispatch]);
useEffect(() => {
if (user && !user.permissions.canViewUsers) {
// No permissions - Bounce to home page
navigate('/');
}
}, [user]);
useEffect(() => {
if (user) {
// Get selected user
getUser(props.email || '')
.then((selectedUser) => setSelectedUser(selectedUser))
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [user, props.email, dispatch]);
useEffect(() => {
if (bookings) {
// Sort it!
setSortedBookings(sortData([...bookings], sortBy, sortOrder));
}
}, [bookings, sortBy, sortOrder]);
useEffect(() => {
if (bookings) {
// Wait for global state to be ready
setLoading(false);
}
}, [bookings]);
// Handlers
const handleSort = (key: keyof Booking) => {
if (key === sortBy) {
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
} else {
setSortBy(key);
}
};
const getAllBookings = useCallback(() => {
if (state.user) {
getBookings({ user: state.user.email })
.then((data) => {
// Split for previous and upcoming
setBookings(data);
})
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [state.user, dispatch]);
const handleCancelBooking = (booking: Booking) => {
cancelBooking(booking.id, booking.user)
.then(() => {
// Clear selected booking
setBookingToCancel(undefined);
// Retrieve updated bookings
getAllBookings();
// Show confirmation alert
dispatch({
type: 'SET_ALERT',
payload: {
message: 'Booking cancelled',
color: 'success',
},
});
})
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
};
// Render
if (!user) {
return null;
}
return (
<AdminLayout currentRoute="users">
<UserBookingsStyles>
{loading || !selectedUser ? (
<Loading />
) : (
<>
<h3>User Bookings</h3>
<Paper square className="table-container">
<h4>{selectedUser.email}</h4>
<TableContainer className="table">
<Table>
<TableHead>
<TableRow>
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'office'}
direction={sortOrder}
onClick={() => handleSort('office')}
>
Office
</TableSortLabel>
</TableCell>
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'date'}
direction={sortOrder}
onClick={() => handleSort('date')}
>
Date
</TableSortLabel>
</TableCell>
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'parking'}
direction={sortOrder}
onClick={() => handleSort('parking')}
>
Parking
</TableSortLabel>
</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{sortedBookings && sortedBookings.length > 0 ? (
sortedBookings.map((booking, index) => {
const parsedDate = parseISO(booking.date);
return (
<TableRow key={index}>
<TableCell>{booking.office.name}</TableCell>
<TableCell>
{' '}
{format(
parse(booking.date, 'yyyy-MM-dd', new Date(), DATE_FNS_OPTIONS),
'do LLLL yyyy',
DATE_FNS_OPTIONS
)}
</TableCell>
<TableCell>{booking.parking ? 'Yes' : 'No'}</TableCell>
{isToday(parsedDate) || !isPast(parsedDate) ? (
<TableCell align="right">
<div className="btn-container">
<OurButton
type="submit"
variant="contained"
color="secondary"
size="small"
onClick={() => setBookingToCancel(booking)}
>
Cancel
</OurButton>
</div>
</TableCell>
) : (
<TableCell />
)}
</TableRow>
);
})
) : (
<TableRow>
<TableCell>No bookings found</TableCell>
<TableCell />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Paper>
</>
)}
{bookingToCancel && (
<Dialog fullScreen={fullScreen} open={true} onClose={() => setBookingToCancel(undefined)}>
<DialogTitle>{'Are you sure you want to cancel this booking?'}</DialogTitle>
<DialogContent>
<DialogContentText>
Booking for <strong>{bookingToCancel.user}</strong> on{' '}
<strong>{format(parseISO(bookingToCancel.date), 'do LLLL')}</strong> for{' '}
<strong>{bookingToCancel.office.name}</strong>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setBookingToCancel(undefined)} color="primary" autoFocus>
No
</Button>
<Button
autoFocus
onClick={() => handleCancelBooking(bookingToCancel)}
color="primary"
>
Yes
</Button>
</DialogActions>
</Dialog>
)}
</UserBookingsStyles>
</AdminLayout>
);
}
Example #10
Source File: Users.tsx From office-booker with MIT License | 4 votes |
Users: React.FC<RouteComponentProps> = () => {
// Global state
const { state, dispatch } = useContext(AppContext);
const { config, user } = state;
// Local state
const [loading, setLoading] = useState(true);
const [dbUsers, setDbUsers] = useState<User[] | undefined>(undefined);
const [sortedUsers, setSortedUsers] = useState<User[] | undefined>(undefined);
const [paginationToken, setPaginationToken] = useState<string | undefined>();
const [selectedFilter, setSelectedFilter] = useState<UserFilter>({
user: 'active',
});
const [email, setEmail] = useState<string>('');
const [sortBy, setSortBy] = useState<keyof User>('email');
const [sortOrder, setSortOrder] = useState<SortOrder>('asc');
const [showAddUser, setShowAddUser] = useState(false);
const [addUserEmail, setAddUserEmail] = useState('');
const [addUserError, setAddUserError] = useState(false);
// Effects
useEffect(() => {
// Retrieve results
queryUsers(userFilterToQuery(selectedFilter))
.then((result) => {
setDbUsers(result.users);
setPaginationToken(result.paginationToken);
})
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}, [selectedFilter, dispatch]);
useEffect(() => {
if (dbUsers) {
// Sort it!
setSortedUsers(sortData([...dbUsers], sortBy, sortOrder));
}
}, [dbUsers, sortBy, sortOrder]);
useEffect(() => {
if (loading && sortedUsers) {
// Wait for local state to be ready
setLoading(false);
}
}, [loading, sortedUsers]);
useEffect(() => {
// Only allow the following
// - Different to value in local state
// AND
// - Not blank OR blank when there was a previous value
const sanitisedEmail = email.trim().toLowerCase();
if (
sanitisedEmail !== selectedFilter.email &&
(sanitisedEmail !== '' || (selectedFilter.email && sanitisedEmail === ''))
) {
// Search after typing has stopped
const searchEmailTimer = setTimeout(() => {
setSelectedFilter((filter) => ({
...filter,
email: sanitisedEmail,
}));
}, 1000);
// Cleanup
return () => clearTimeout(searchEmailTimer);
}
return;
}, [selectedFilter.email, email]);
// Handlers
const handleChangeUser = (e: React.ChangeEvent<{ value: unknown }>) => {
const user = e.target.value as string;
if (
user === 'active' ||
user === 'System Admin' ||
user === 'Office Admin' ||
user === 'custom' ||
user === 'Auto Approved'
) {
setSelectedFilter((filter) => ({ ...filter, user }));
}
};
const handleSort = (key: keyof User) => {
if (key === sortBy) {
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
} else {
setSortBy(key);
}
};
const handleLoadMore = () => {
if (paginationToken && selectedFilter.user === 'active') {
// Clear the current token
setPaginationToken(undefined);
// Retrieve data after pagination token
queryUsers({}, paginationToken)
.then((result) => {
// Merge new results with existing
setDbUsers((previousUsers) => [...(previousUsers ?? []), ...result.users]);
setPaginationToken(result.paginationToken);
})
.catch((error) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(error),
color: 'error',
},
})
);
}
};
const handleAdduser = () => {
if (validateEmail(state.config?.emailRegex, addUserEmail)) {
setAddUserError(false);
navigate(`/admin/users/${addUserEmail}`);
} else {
setAddUserError(true);
}
};
// Render
if (!user) {
return null;
}
return (
<AdminLayout currentRoute="users">
<UsersStyles>
{loading ? (
<Loading />
) : (
<>
<h3>Users</h3>
<OurButton
startIcon={<AddCircleIcon />}
type="submit"
color="secondary"
onClick={() => setShowAddUser(true)}
variant="contained"
size="small"
>
New user
</OurButton>
<Paper square className="table-container">
<section className="filters">
<div className="filter-role">
<FormControl>
<InputLabel>User</InputLabel>
<Select value={selectedFilter.user} onChange={handleChangeUser}>
<MenuItem value="active">All active users</MenuItem>
<MenuItem value="System Admin">System Admins</MenuItem>
<MenuItem value="Office Admin">Office Admins</MenuItem>
<MenuItem value="custom">With custom quota</MenuItem>
{config?.reasonToBookRequired && (
<MenuItem value="Auto Approved">With auto approved</MenuItem>
)}
</Select>
</FormControl>
</div>
<div className="search-user">
<TextField
placeholder="Start of email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</div>
</section>
{selectedFilter.user === 'active' && (
<p className="note">
Please note, a user is only considered "active" after logging into the
app the first time.
</p>
)}
<TableContainer className="table">
<Table>
<TableHead>
<TableRow>
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'email'}
direction={sortOrder}
onClick={() => handleSort('email')}
>
User Email
</TableSortLabel>
</TableCell>
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'quota'}
direction={sortOrder}
onClick={() => handleSort('quota')}
>
Quota
</TableSortLabel>
</TableCell>
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'role'}
direction={sortOrder}
onClick={() => handleSort('role')}
>
Role
</TableSortLabel>
</TableCell>
{config?.reasonToBookRequired && (
<TableCell className="table-header">
<TableSortLabel
active={sortBy === 'autoApproved'}
direction={sortOrder}
onClick={() => handleSort('autoApproved')}
>
Auto Approved
</TableSortLabel>
</TableCell>
)}
<TableCell className="table-header" />
</TableRow>
</TableHead>
<TableBody>
{sortedUsers && sortedUsers.length > 0 ? (
sortedUsers.map((user) => (
<TableRow key={user.email}>
<TableCell>{user.email}</TableCell>
<TableCell>{user.quota}</TableCell>
<TableCell>{user.role.name}</TableCell>
{config?.reasonToBookRequired && (
<TableCell>
{user.autoApproved ? <CheckIcon /> : <ClearIcon color="disabled" />}
</TableCell>
)}
<TableCell align="right">
<Tooltip title={`Edit`} arrow>
<IconButton onClick={() => navigate(`/admin/users/${user.email}`)}>
<CreateIcon />
</IconButton>
</Tooltip>
<Tooltip title={`View Bookings`} arrow>
<IconButton
onClick={() => navigate(`/admin/users/bookings/${user.email}`)}
>
<TodayIcon />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell>No users found</TableCell>
<TableCell />
<TableCell />
<TableCell />
{config?.reasonToBookRequired && <TableCell />}
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{paginationToken && (
<section className="load-more-container">
<OurButton onClick={handleLoadMore} color="primary" variant="contained">
Load More
</OurButton>
</section>
)}
{selectedFilter.user === 'active' &&
selectedFilter.email !== undefined &&
validateEmail(state.config?.emailRegex, selectedFilter.email) &&
dbUsers?.length === 0 && (
<section className="unregistered-user">
<div className="link">
<p>
User <span>{selectedFilter.email}</span> not yet registered
</p>
<OurButton
startIcon={<AddCircleIcon />}
type="submit"
variant="contained"
color="primary"
onClick={() => navigate(`/admin/users/${selectedFilter.email}`)}
size="small"
>
Add user
</OurButton>
</div>
<p>
Please note the user will not be considered "active" until they
login for the first time.
</p>
</section>
)}
</Paper>
<Dialog open={showAddUser} onClose={() => setShowAddUser(false)}>
<DialogTitle>Enter the email address for the user you wish to create.</DialogTitle>
<DialogContent>
<DialogContentText>
Please note the user will not be considered "active" until they login
for the first time.
</DialogContentText>
<TextField
autoFocus
label="Email Address"
type="email"
value={addUserEmail}
onChange={(e) => setAddUserEmail(e.target.value)}
error={addUserError}
helperText={addUserError && `Invalid email address`}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowAddUser(false)} color="primary" autoFocus>
Cancel
</Button>
<Button
startIcon={<AddCircleIcon />}
variant="contained"
onClick={() => handleAdduser()}
color="secondary"
>
New user
</Button>
</DialogActions>
</Dialog>
</>
)}
</UsersStyles>
</AdminLayout>
);
}
Example #11
Source File: Help.tsx From office-booker with MIT License | 4 votes |
Help: React.FC<RouteComponentProps> = () => {
// Global state
const { state, dispatch } = useContext(AppContext);
const { user, office } = state;
const [currentOffice, setCurrentOffice] = useState<OfficeWithSlots | undefined>();
useEffect(() => {
if (user && office && 'id' in office) {
getOffice(office.id)
.then(setCurrentOffice)
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
}
if (user && office && 'name' in office) {
getOffices()
.then((offices) =>
dispatch({
type: 'SET_OFFICE',
payload: offices.find((o) => o.name === office.name),
})
)
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
}
}, [user, office, dispatch]);
// Effects
useEffect(() => {
// Retrieve user if not already available
if (!user) {
// Retrieve cognito session
getAuthState()
.then((username) => {
// Retrieve DB user
if (username) {
getUserCached(username)
.then((data) =>
dispatch({
type: 'SET_USER',
payload: data,
})
)
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
}
})
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
}
}, [dispatch, user]);
useEffect(() => {
// Validate global office
if (user && office) {
getOffices()
.then((data) => {
// Validate and update local storage if not
const findOffice =
'id' in office
? data.find((o) => o.id === office.id)
: data.find((o) => o.name === office.name);
if (!findOffice) {
dispatch({
type: 'SET_OFFICE',
payload: undefined,
});
}
})
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
}
}, [dispatch, user, office]);
// Handlers
const handleClearOffice = () => {
// Update global state
dispatch({
type: 'SET_OFFICE',
payload: undefined,
});
// Change page if required
navigate('/');
};
// Render
if (!state.config) {
return null;
}
return (
<Layout>
<HelpStyles>
<h2>Help</h2>
<p>
The purpose of the Office Booker app is to help coordinate the safe use of office
facilities by proactively managing demand.
</p>
<h3>General Information</h3>
<p>Bookings can only be made {state.config.advancedBookingDays} days in advance.</p>
{user && currentOffice && (
<div className="change-office">
<p>
You are currently booking for <span>{currentOffice.name}</span>.
</p>
<Link
component="button"
underline="always"
color="primary"
onClick={() => handleClearOffice()}
>
Change office
</Link>
</div>
)}
<h3>Credits</h3>
<p>
Office Booker was built by The Lab at O2, visit us on{' '}
<a href="https://github.com/o2Labs/" rel="noopener noreferrer" target="_blank">
GitHub
</a>
.
</p>
<p>Icon made by Freepik from www.flaticon.com</p>
</HelpStyles>
</Layout>
);
}
Example #12
Source File: MakeBooking.tsx From office-booker with MIT License | 4 votes |
MakeBooking: React.FC<Props> = (props) => {
const { office, bookings, refreshBookings } = props;
// Global state
const { state, dispatch } = useContext(AppContext);
const { config, user } = state;
// Local state
const [weeks, setWeeks] = useState<Week[]>([]);
const [rows, setRows] = useState<Row[]>([]);
const [selectedWeek, setSelectedWeek] = useState<number | undefined>();
const [buttonsLoading, setButtonsLoading] = useState(true);
const [showTodayConfirmation, setShowTodayConfirmation] = useState(false);
const [showReasonConfirmation, setShowReasonConfirmation] = useState(false);
const [bookingDate, setBookingDate] = useState<Date | undefined>();
const [bookingParking, setBookingParking] = useState(false);
const [bookingReason, setBookingReason] = useState<string | undefined>();
const [isAutoApprovedUser, setIsAutoApprovedUser] = useState<boolean>(false);
const [searchedUser, setSearchedUser] = useState<User | undefined>();
// Refs
const reloadTimerRef = useRef<ReturnType<typeof setInterval> | undefined>();
// Helper
const setReloadTimer = useCallback(() => {
reloadTimerRef.current = setInterval(() => {
setButtonsLoading(true);
refreshBookings();
}, RELOAD_TIME);
}, [refreshBookings]);
const resetReloadTimer = () => {
reloadTimerRef.current && clearInterval(reloadTimerRef.current);
setReloadTimer();
};
const isReasonRequired = () => config?.reasonToBookRequired && !isAutoApprovedUser;
// Effects
useEffect(() => {
// Periodically refresh bookings
setReloadTimer();
// Handle browser visibility
const handleVisibilityChange = () => {
if (document.hidden) {
// Clear timer
reloadTimerRef.current && clearInterval(reloadTimerRef.current);
} else {
// Reload data & restart timer
setButtonsLoading(true);
setReloadTimer();
refreshBookings();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
reloadTimerRef.current && clearInterval(reloadTimerRef.current);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [setReloadTimer, refreshBookings]);
useEffect(() => {
// Set week numbers from available slots
const weeks: Week[] = [];
office.slots.forEach((s) => {
// Get week
const date = parse(s.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS);
const weekStart = startOfWeek(date, DATE_FNS_OPTIONS);
// Is it already in the list?
if (!weeks.find((w) => w.startOfWeek.getTime() === weekStart.getTime())) {
// Find total user bookings for this week
const userBookings = bookings.filter((b) =>
isSameWeek(parse(b.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS), date, DATE_FNS_OPTIONS)
);
weeks.push({
startOfWeek: weekStart,
bookings: userBookings.length,
});
}
});
setWeeks(weeks);
}, [bookings, office]);
useEffect(() => {
if (weeks.length > 0) {
setButtonsLoading(false);
if (selectedWeek === undefined) {
setSelectedWeek(0);
}
}
}, [selectedWeek, weeks]);
const getWeekLabel = (week: Week) => {
const startDate = week.startOfWeek;
const endDate = endOfWeek(startDate, DATE_FNS_OPTIONS);
// Are dates in the same month
const sameMonth = isSameMonth(startDate, endDate);
// Set label
const startLabel = format(startDate, 'LLL d', DATE_FNS_OPTIONS);
const endLabel = sameMonth
? format(endDate, 'd', DATE_FNS_OPTIONS)
: format(endDate, 'LLL d', DATE_FNS_OPTIONS);
return `${startLabel} - ${endLabel}`;
};
useEffect(() => {
if (user && weeks && weeks.length > 0) {
const { id, quota: officeQuota, parkingQuota, slots } = office;
const { quota: userQuota } = user;
const rows: Row[] = [];
// For each week
weeks.forEach((w) => {
// Find all days in the week
const startDate = w.startOfWeek;
const endDate = endOfWeek(w.startOfWeek, DATE_FNS_OPTIONS);
const weekDays = eachDayOfInterval({
start: startDate,
end: endDate,
});
// For each day
const days: Row['days'] = [];
weekDays.forEach((d) => {
// Is the date in the slots?
const slot = slots.find((s) => format(d, 'y-MM-dd', DATE_FNS_OPTIONS) === s.date);
if (slot) {
// Calculate available spaces
const available = officeQuota - slot.booked;
const availableCarPark = parkingQuota - slot.bookedParking;
// Find any user booking for this office/slot
const booking = bookings.find((b) => b.office.id === id && b.date === slot.date);
// Total user bookings for this week
const userWeekBookings = bookings.filter((b) =>
isSameWeek(
parse(b.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
parse(slot.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
DATE_FNS_OPTIONS
)
);
// Add day
days.push({
date: d,
isBookable: true,
available,
availableCarPark,
userCanBook: available > 0 && userWeekBookings.length < userQuota,
booking,
});
} else {
// Did we have a booking on this day?
const booking = bookings.find(
(b) => b.office.id === id && b.date === format(d, 'y-MM-dd', DATE_FNS_OPTIONS)
);
// Not in range
days.push({
date: d,
isBookable: false,
booking,
});
}
});
// Add to rows
rows.push({
week: w,
start: startDate,
end: endDate,
days,
});
});
setRows(rows);
}
}, [bookings, office, user, weeks]);
useEffect(() => {
if (user && config?.reasonToBookRequired) {
// Get selected user
getUser(user?.email)
.then((searchedUser) => setSearchedUser(searchedUser))
.catch((err) => {
// Handle errors
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [user, dispatch, config?.reasonToBookRequired]);
useEffect(() => {
if (user) {
if (searchedUser?.autoApproved) {
setIsAutoApprovedUser(true);
}
}
}, [searchedUser?.autoApproved, user]);
// Handlers
const handleChangeWeek = (direction: 'forward' | 'backward') => {
// Find index of current selected
if (selectedWeek !== undefined) {
setSelectedWeek(
direction === 'forward'
? selectedWeek + 1
: direction === 'backward'
? selectedWeek - 1
: selectedWeek
);
}
};
const confirmTodayBooking = (
handleCreateBooking: (date: Date, withParking: boolean) => void,
date: Date,
withParking: boolean
) => {
if (isReasonRequired()) {
setBookingDate(date);
setBookingParking(withParking);
if (isToday(date)) {
setShowTodayConfirmation(true);
} else {
setShowReasonConfirmation(true);
}
} else {
handleCreateBooking(date, withParking);
}
};
const handleCreateBooking = (date: Date, withParking: boolean, reason?: string) => {
if (user) {
setButtonsLoading(true);
// Reset auto-reload timer
resetReloadTimer();
// Create new booking
const formattedDate = format(date, 'yyyy-MM-dd', DATE_FNS_OPTIONS);
createBooking(user.email, formattedDate, office, withParking, reason)
.then(() => {
// Clear form
setBookingDate(undefined);
setBookingParking(false);
setBookingReason(undefined);
// Refresh DB
refreshBookings();
})
.catch((err) => {
// Refresh DB
refreshBookings();
// Handle errors
setButtonsLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
};
const handleCancelBooking = (booking: Booking) => {
if (user) {
setButtonsLoading(true);
// Reset auto-reload timer
resetReloadTimer();
// Cancel existing booking
cancelBooking(booking.id, user.email)
.then(() => refreshBookings())
.catch((err) => {
// Refresh DB
refreshBookings();
// Handle errors
setButtonsLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
};
const handleClearOffice = () => {
// Update global state
dispatch({
type: 'SET_OFFICE',
payload: undefined,
});
};
// Render
if (!user) {
return null;
}
return (
<MakeBookingStyles>
<div className="title">
<h2>{office.name}</h2>
<Link
component="button"
underline="always"
color="primary"
onClick={() => handleClearOffice()}
>
Change office
</Link>
</div>
{isReasonRequired() && (
<p className="notice">
You will be asked to record your reason for going to work when you make a booking.
</p>
)}
<ul>
<li>
You can make <span>{user.quota}</span> booking per week.
</li>
<li>
{office.name} has a daily capacity of <span>{office.quota}</span>
{office.parkingQuota > 0 ? (
<>
{` `}and car park capacity of <span>{office.parkingQuota}</span>.
</>
) : (
`.`
)}
</li>
</ul>
{weeks && weeks.length > 0 && selectedWeek !== undefined && (
<>
<Paper square className="bookings">
<div className="menu">
<div className="back">
<IconButton
disabled={selectedWeek === 0}
onClick={() => handleChangeWeek('backward')}
size="small"
>
<ArrowLeftIcon
fontSize="inherit"
className="icon"
color={selectedWeek === 0 ? 'disabled' : 'secondary'}
/>
</IconButton>
</div>
<div className="date">
<h3>{getWeekLabel(weeks[selectedWeek])}</h3>
</div>
<div className="forward">
<IconButton
disabled={selectedWeek === weeks.length - 1}
onClick={() => handleChangeWeek('forward')}
size="small"
>
<ArrowRightIcon
fontSize="inherit"
className="icon"
color={selectedWeek === weeks.length - 1 ? 'disabled' : 'secondary'}
/>
</IconButton>
</div>
<div className="refresh">
<Tooltip title="Refresh availability">
<IconButton
onClick={() => {
setButtonsLoading(true);
resetReloadTimer();
refreshBookings();
}}
disabled={buttonsLoading}
>
<CachedIcon color="primary" />
</IconButton>
</Tooltip>
</div>
</div>
<div className="details">
<p className="quota">
<span>{user.quota - weeks[selectedWeek].bookings}</span>{' '}
{user.quota - weeks[selectedWeek].bookings === 1 ? 'booking' : 'bookings'} remaining
</p>
<p className="upcoming-bookings">
<Link component="button" color="inherit" onClick={() => navigate('/bookings')}>
View bookings
</Link>
</p>
</div>
{rows
.filter((row, index) => selectedWeek === index)
.map((row, weekIndex) => (
<div key={weekIndex} className="grid">
{row.days.map((day, dayIndex) => (
<div
key={dayIndex}
className="row"
data-today={isToday(day.date)}
data-bookable={
day.isBookable && (day.userCanBook || day.booking) ? true : false
}
>
<div className="left">
<p className="date">{format(day.date, 'E do', DATE_FNS_OPTIONS)}</p>
</div>
{day.isBookable && (
<div className="right">
{day.booking ? (
<>
<Tooltip
title={
isToday(day.date)
? "Today's booking can only be cancelled by administrators"
: ''
}
placement="top-end"
>
<Link
component="button"
underline="always"
className={`${buttonsLoading ? 'loading ' : ''}${
isToday(day.date) ? 'disabled ' : ''
}cancelBtn`}
onClick={() =>
!buttonsLoading &&
day.booking &&
!isToday(day.date) &&
handleCancelBooking(day.booking)
}
>
Cancel
</Link>
</Tooltip>
<OurButton
size="small"
variant="contained"
color="primary"
onClick={() => {
navigate(`./booking/${day.booking?.id}`);
}}
endIcon={
day.booking?.parking ? (
<EmojiTransportationIcon />
) : (
<BusinessIcon />
)
}
>
View Pass
</OurButton>
</>
) : (
<div className="no-booking">
<div className="availability">
<BookingStatus
officeQuota={office.quota}
officeAvailable={day.available}
parkingQuota={office.parkingQuota}
parkingAvailable={day.availableCarPark}
/>
</div>
{day.userCanBook && (
<div className="book">
<BookButton
onClick={(withParking) =>
confirmTodayBooking(
handleCreateBooking,
day.date,
withParking
)
}
parkingQuota={office.parkingQuota}
parkingAvailable={day.availableCarPark}
buttonsLoading={buttonsLoading}
/>
</div>
)}
</div>
)}
</div>
)}
</div>
))}
</div>
))}
</Paper>
<Dialog
open={showTodayConfirmation}
onClose={() => setShowTodayConfirmation(false)}
disableBackdropClick
>
<DialogContent>
<DialogContentText color="secondary">
Today's booking can only be cancelled by administrators
</DialogContentText>
</DialogContent>
<DialogActions>
<OurButton
onClick={() => setShowTodayConfirmation(false)}
size="small"
color="primary"
>
Cancel
</OurButton>
<OurButton
onClick={() => {
if (isReasonRequired()) {
setShowTodayConfirmation(false);
setShowReasonConfirmation(true);
} else {
bookingDate && handleCreateBooking(bookingDate, bookingParking);
}
}}
variant="contained"
size="small"
color="secondary"
>
Confirm{bookingParking ? ` + Parking` : null}
</OurButton>
</DialogActions>
</Dialog>
<Dialog
open={showReasonConfirmation}
onClose={() => setShowReasonConfirmation(false)}
disableBackdropClick
>
<DialogContent>
<DialogContentText color="secondary">
You can only leave home for work purposes where it is unreasonable for you to do
your job from home. Please briefly explain why you cannot work from home.
</DialogContentText>
<TextField
autoFocus
label="Details"
type="text"
margin="normal"
fullWidth
multiline
required
value={bookingReason}
onChange={(e) => setBookingReason(e.target.value)}
/>
</DialogContent>
<DialogActions>
<OurButton
onClick={() => setShowReasonConfirmation(false)}
size="small"
color="primary"
>
Cancel
</OurButton>
<OurButton
onClick={() => {
if (bookingDate && bookingReason && bookingReason.trim().length > 0) {
setShowReasonConfirmation(false);
handleCreateBooking(bookingDate, bookingParking, bookingReason);
}
}}
variant="contained"
size="small"
color="secondary"
>
Confirm{bookingParking ? ` + Parking` : null}
</OurButton>
</DialogActions>
</Dialog>
</>
)}
</MakeBookingStyles>
);
}
Example #13
Source File: UpcomingBookings.tsx From office-booker with MIT License | 4 votes |
UpcomingBookings: React.FC<RouteComponentProps> = () => {
// Global state
const { state, dispatch } = useContext(AppContext);
// Local state
const [loading, setLoading] = useState(true);
const [upcomingBookings, setUpcomingBookings] = useState<Booking[] | undefined>();
const [previousBookings, setPreviousBookings] = useState<Booking[] | undefined>();
// Effects
useEffect(() => {
if (state.user) {
getBookings({ user: state.user.email })
.then((data) => {
// Split for previous and upcoming
const today = format(startOfDay(new Date()), 'yyyy-MM-dd');
setUpcomingBookings(data.filter((b) => b.date >= today));
setPreviousBookings(data.filter((b) => b.date < today));
})
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [state.user, dispatch]);
useEffect(() => {
// Find booking
if (upcomingBookings && previousBookings) {
setLoading(false);
}
}, [upcomingBookings, previousBookings]);
// Render
return (
<Layout>
{loading ? (
<LoadingSpinner />
) : (
<UpcomingBookingsStyles>
<h2>Upcoming Bookings</h2>
{upcomingBookings && upcomingBookings.length > 0 ? (
<Paper square className="bookings">
{upcomingBookings.map((row, index) => (
<div key={row.id} className="grid">
<div key={index} className="row">
<div className="left">
<p className="date">
{format(
parse(row.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
'do LLL',
DATE_FNS_OPTIONS
)}
</p>
<p className="office">{row.office.name}</p>
</div>
<div className="right">
<OurButton
size="small"
variant="contained"
color="primary"
onClick={() => {
navigate(`./booking/${row?.id}`);
}}
endIcon={row.parking ? <EmojiTransportationIcon /> : <BusinessIcon />}
>
View Pass
</OurButton>
</div>
</div>
</div>
))}
</Paper>
) : (
<p>No upcoming bookings found.</p>
)}
{previousBookings && previousBookings.length > 0 && (
<>
<h3>Previous Bookings</h3>
<ul>
{previousBookings.map((row) => (
<li key={row.id}>
{format(
parse(row.date, 'yyyy-MM-dd', new Date(), DATE_FNS_OPTIONS),
'do LLLL',
DATE_FNS_OPTIONS
)}
{` `}
<span>at {row.office.name}</span>
{` `}
<span>{row.parking ? '(+ Parking)' : ''}</span>
</li>
))}
</ul>
</>
)}
<div className="button">
<OurButton
type="button"
variant="contained"
color="primary"
onClick={() => {
navigate(`/`);
}}
>
Home
</OurButton>
</div>
</UpcomingBookingsStyles>
)}
</Layout>
);
}
Example #14
Source File: ViewBooking.tsx From office-booker with MIT License | 4 votes |
ViewBooking: React.FC<RouteComponentProps<Props>> = (props) => {
// Global state
const { state, dispatch } = useContext(AppContext);
const { user } = state;
// Local state
const [loading, setLoading] = useState(true);
const [booking, setBooking] = useState<Booking | undefined>(undefined);
// Effects
useEffect(() => {
if (user) {
getBookings({ user: user.email })
.then((data) => {
const findBooking = data.find((b) => b.id === props.id);
if (findBooking) {
setBooking(findBooking);
} else {
setLoading(false);
// Bounce back to home
setTimeout(() => navigate('/'), 2500);
}
})
.catch((err) => {
// Handle errors
setLoading(false);
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
});
});
}
}, [dispatch, user, props.id]);
useEffect(() => {
// Finished loading
if (booking) {
setLoading(false);
}
}, [booking]);
// Render
if (!user) {
return null;
}
const emailSplit = user.email.split('@');
return (
<Layout>
{loading ? (
<LoadingSpinner />
) : (
<ViewBookingStyles>
{booking ? (
<Paper square className="card">
<p className="day">
{format(
parse(booking.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
'eeee',
DATE_FNS_OPTIONS
)}
</p>
<h2>
{format(
parse(booking.date, 'y-MM-dd', new Date(), DATE_FNS_OPTIONS),
'do LLLL',
DATE_FNS_OPTIONS
)}
</h2>
<h3>{booking.office.name}</h3>
{booking.parking && <p className="parking">+ parking</p>}
<div className="breaker"></div>
<h4>{emailSplit[0]}</h4>
<p className="domain">@{emailSplit[1]}</p>
</Paper>
) : (
<p className="not-found">
Sorry, we can't find your booking reference. <br />
Redirecting you to home...
</p>
)}
<div className="button">
<OurButton
type="submit"
variant="contained"
color="primary"
onClick={() => {
navigate(`/`);
}}
>
Home
</OurButton>
</div>
</ViewBookingStyles>
)}
</Layout>
);
}
Example #15
Source File: Footer.tsx From office-booker with MIT License | 4 votes |
Footer: React.FC = () => {
// Global state
const { state, dispatch } = useContext(AppContext);
const { user } = state;
// Effects
useEffect(() => {
// Retrieve user if not already available
if (!user) {
// Retrieve cognito session
getAuthState()
.then((username) => {
// Retrieve DB user
if (username) {
// Retrieve cached user
getUserCached(username)
.then((data) =>
dispatch({
type: 'SET_USER',
payload: data,
})
)
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
}
})
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
}
}, [dispatch, user]);
// Handlers
const handleSignOut = () => {
// Sign out
signOut()
.then(() => {
dispatch({ type: 'SET_USER', payload: undefined });
navigate('/');
})
.catch((err) =>
dispatch({
type: 'SET_ALERT',
payload: {
message: formatError(err),
color: 'error',
},
})
);
};
// Render
return (
<FooterStyles>
<div className="link">
<ReachLink to="/help">Help</ReachLink>
</div>
<div className="link">
<ReachLink to="/privacy">Privacy</ReachLink>
</div>
{user?.permissions.canViewAdminPanel && (
<div className="link">
<ReachLink to="/admin">Admin</ReachLink>
</div>
)}
{user && (
<div className="link">
<Link component="button" color="inherit" onClick={() => handleSignOut()}>
Sign Out
</Link>
</div>
)}
</FooterStyles>
);
}