diff --git a/Cargo.toml b/Cargo.toml
index dd193d7..2bb34e9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-axum = { version = "0.7.3", features = ["macros"] }
+axum = { version = "0.7.3", features = ["macros", "tokio"] }
chrono = { version = "0.4.31", features = ["serde"] }
dotenv = "0.15.0"
futures = "0.3.30"
diff --git a/frontend/index.html b/frontend/index.html
index e4b78ea..edf58be 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,9 +2,9 @@
-
- Vite + React + TS
+ findtheti.me
+
diff --git a/frontend/package.json b/frontend/package.json
index 6c1b124..0290930 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,5 +1,5 @@
{
- "name": "frontend",
+ "name": "findtheti.me",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -21,6 +21,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-github-corner": "^2.5.0",
+ "react-hot-toast": "^2.4.1",
"react-router-dom": "^6.21.1"
},
"devDependencies": {
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 44864c4..3e6d8c6 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3,6 +3,7 @@ import './App.css'
import RootLayout from './pages/RootLayout'
import NewEventPage from './pages/NewEventPage'
import ExistingEventPage from './pages/ExistingEventPage'
+import ThankYouPage from './pages/ThankYouPage'
function App() {
return (
@@ -10,6 +11,7 @@ function App() {
} />
} />
+ } />
)
diff --git a/frontend/src/assets/calendar-color-icon.png b/frontend/src/assets/calendar-color-icon.png
new file mode 100644
index 0000000..4f29e66
Binary files /dev/null and b/frontend/src/assets/calendar-color-icon.png differ
diff --git a/frontend/src/components/AvailabilityPicker.tsx b/frontend/src/components/AvailabilityPicker.tsx
index bf9b3f6..ebee96a 100644
--- a/frontend/src/components/AvailabilityPicker.tsx
+++ b/frontend/src/components/AvailabilityPicker.tsx
@@ -1,28 +1,19 @@
-import { Box, Card, Divider, Stack, Typography } from "@mui/material";
-import dayjs, { Dayjs } from "dayjs";
-import { MouseEvent, useEffect, useState } from "react";
-import * as utc from 'dayjs/plugin/utc';
-import * as timezone from 'dayjs/plugin/timezone';
-import * as localizedFormat from 'dayjs/plugin/localizedFormat';
-import utils from "../utils";
-import { EventTypes } from "../types/Event";
+import { Box, Stack } from "@mui/material";
+import { MouseEvent, useState } from "react";
import "./css/AvailabilityPicker.css";
-import classNames from 'classnames';
-// import { alpha } from '@material-ui/core/styles/colorManipulator';
+import { AvailabilityDay, OthersDay, OthersDays } from "../types/Availabilities";
+import utils from "../utils";
+import AvailabilityPickerDay from "./AvailabilityPickerDay";
+import dayjs, { Dayjs } from "dayjs";
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(localizedFormat)
-type AvailabilityTime = {
- fromTime: Dayjs,
- toTime: Dayjs
-}
-
-type AvailabilityDay = {
- forDate: Dayjs,
- availableTimes: AvailabilityTime[]
-}
+const HALFHOUR_DISPLAY_HEIGHT: number = 15;
type GhostPreviewProps = {
top: number,
@@ -31,173 +22,19 @@ type GhostPreviewProps = {
height: number
}
-const HALFHOUR_DISPLAY_HEIGHT: number = 15;
-
-const Hour = (props: {
- dateTime: Dayjs,
- isFullHourSelected: boolean,
- isHalfHourSelected: boolean,
- onMouseEnterHalfhour: (e: MouseEvent, time: Dayjs) => void,
- onMouseClickOnHalfhour: (time: Dayjs) => void
-}) => {
- let isEvenHour = props.dateTime.hour() % 2 == 0;
-
- return (
-
- props.onMouseEnterHalfhour(e, props.dateTime)}
- onClick={(_) => props.onMouseClickOnHalfhour(props.dateTime)}
- >
-
- { utils.formatTimeFromHourOfDay(props.dateTime.hour(), 0) }
-
-
- props.onMouseEnterHalfhour(e, props.dateTime.add(30, "minutes"))}
- onClick={(_) => props.onMouseClickOnHalfhour(props.dateTime.add(30, "minutes"))}
- />
-
- );
-}
-
-const isSelectedAvailability = (day: AvailabilityDay, time: Dayjs): boolean => {
- return day.availableTimes.some(t => t.fromTime.unix() <= time.unix() && time.unix() <= t.toTime.unix());
-}
-
-const Day = (props: {
- day: AvailabilityDay,
- eventType: String,
- onMouseEnterHalfhour: (e: MouseEvent, time: dayjs.Dayjs) => void,
- onMouseClickHalfhour: (day: AvailabilityDay, time: dayjs.Dayjs) => void
-}) => {
-
- const generateHours = (): JSX.Element[] => {
- let hours: JSX.Element[] = [];
-
- for (var i = 0; i < 24; i++) {
- let time = props.day.forDate.set("hour", i);
- hours.push(
- , time: dayjs.Dayjs): void => {
- props.onMouseEnterHalfhour(e, time);
- }}
- onMouseClickOnHalfhour={(time: dayjs.Dayjs): void => {
- props.onMouseClickHalfhour(props.day, time);
- }}
- />
- );
- }
-
- return hours;
- }
-
- return (
-
-
- {
- (props.eventType === EventTypes.WEEK) &&
-
- { props.day.forDate.format("dddd") }
-
- }
- {
- (props.eventType === EventTypes.DAY) &&
-
- Any Day
-
- }
- {
- (props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) &&
-
- { props.day.forDate.format("LL") }
-
- }
-
-
-
-
- {generateHours()}
-
-
- );
-}
-
const AvailabilityPicker = (props: {
- fromDate: Dayjs,
- toDate: Dayjs,
+ days: AvailabilityDay[],
+ setDays: (days: AvailabilityDay[]) => void,
+ othersAvailabilities: OthersDays[],
eventType: String,
- availabilityDurationInMinutes: number
+ availabilityDurationInMinutes: number,
}) => {
-
- const [days, setDays] = useState([]);
+
const [ghostPreviewProps, setGhostPreviewProps] = useState();
- useEffect(() => {
- let localTimezone = dayjs.tz.guess();
-
- let localFromDate = props.fromDate.tz(localTimezone);
- let localToDate = props.toDate.tz(localTimezone);
-
- switch (props.eventType) {
- case EventTypes.SPECIFIC_DATE: {
- createAvailabilitiesBasedOnInitialDate(localFromDate, 1);
- break;
- }
- case EventTypes.DATE_RANGE: {
- createAvailabilitiesBasedOnInitialDate(localFromDate, Math.abs(localFromDate.diff(localToDate, "day", false)));
- break;
- }
- case EventTypes.DAY: {
- createAvailabilitiesBasedOnUnspecifiedInitialDate(1, localTimezone);
- break;
- }
- case EventTypes.WEEK: {
- createAvailabilitiesBasedOnUnspecifiedInitialDate(7, localTimezone);
- break;
- }
- }
- }, [props]);
-
- const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string) => {
- createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays);
- }
-
- const createAvailabilitiesBasedOnInitialDate = (date: Dayjs, numberOfDays: number) => {
- let availabilities: AvailabilityDay[] = [];
-
- for (var i: number = 0; i < numberOfDays; i++) {
- let availability: AvailabilityDay = {
- forDate: date.add(i, "day").startOf("day"),
- availableTimes: []
- }
-
- availabilities.push(availability);
- }
-
- setDays(availabilities);
- }
-
const displayGhostPeriod = (e: MouseEvent, time: Dayjs) => {
- let timeInMinutes = (time.hour() * 60) + time.minute();
- let timeLeftInDayLessThanDuration = (timeInMinutes + props.availabilityDurationInMinutes) > 24 * 60;
+ let timeInMinutes = (time.hour() * 60.0) + time.minute();
+ let timeLeftInDayLessThanDuration = (timeInMinutes + props.availabilityDurationInMinutes) > 24.0 * 60.0;
if (timeLeftInDayLessThanDuration) {
return;
@@ -215,16 +52,34 @@ const AvailabilityPicker = (props: {
// @ts-ignore
left: e.target?.offsetLeft - scrollLeft,
width: element.width,
- height: (props.availabilityDurationInMinutes/60) * 2 * HALFHOUR_DISPLAY_HEIGHT
+ height: (props.availabilityDurationInMinutes/60.0) * 2 * HALFHOUR_DISPLAY_HEIGHT
})
}
- const createAvailability = (day: AvailabilityDay, time: Dayjs) => {
+ const deleteAvailability = (day: AvailabilityDay, time: Dayjs) => {
+ let existingTime = day.availableTimes.findIndex(t => utils.dayjsIsBetweenUnixExclusive(t.fromTime, time, t.toTime));
+
+ if (existingTime >= 0) {
+ console.log(`delete ${existingTime} from`, day)
+
+ day.availableTimes.splice(existingTime, 1);
+
+ props.setDays([...props.days]);
+ }
+ }
+
+ const changeAvailability = (day: AvailabilityDay, time: Dayjs, isDelete: boolean) => {
+ if (isDelete) {
+ deleteAvailability(day, time);
+
+ return;
+ }
+
let fromTime = time;
let toTime = time.add(props.availabilityDurationInMinutes, "minutes");
- let existingTimeContainingFrom = day.availableTimes.findIndex(t => (t.fromTime.isBefore(fromTime) || t.fromTime.isSame(fromTime)) && (t.toTime.isAfter(fromTime) || t.toTime.isSame(fromTime)));
- let existingTimeContainingTo = day.availableTimes.findIndex(t => (t.fromTime.isBefore(toTime) || t.fromTime.isSame(toTime)) && (t.toTime.isAfter(toTime) || t.toTime.isSame(toTime)));
+ let existingTimeContainingFrom = day.availableTimes.findIndex(t => utils.dayjsIsBetweenUnixInclusive(t.fromTime, fromTime, t.toTime));
+ let existingTimeContainingTo = day.availableTimes.findIndex(t => utils.dayjsIsBetweenUnixInclusive(t.fromTime, toTime, t.toTime));
// the newly created availability crosses another single one. Both have the same from and to. Do nothing.
if (existingTimeContainingFrom >= 0 && existingTimeContainingTo >= 0 && existingTimeContainingFrom === existingTimeContainingTo) {
@@ -234,16 +89,18 @@ const AvailabilityPicker = (props: {
// the newly created availability crosses 2 existing ones. Combine all of them into a single one.
if (existingTimeContainingFrom >= 0 && existingTimeContainingTo >= 0 && existingTimeContainingFrom !== existingTimeContainingTo) {
let newFrom = day.availableTimes[existingTimeContainingFrom].fromTime;
- let newTo = day.availableTimes[existingTimeContainingFrom].toTime;
+ let newTo = day.availableTimes[existingTimeContainingTo].toTime;
- day.availableTimes.splice(existingTimeContainingFrom);
- day.availableTimes.splice(existingTimeContainingTo);
+ day.availableTimes.splice(existingTimeContainingFrom, 1);
+ day.availableTimes.splice(existingTimeContainingTo, 1);
day.availableTimes.push({
fromTime: newFrom,
toTime: newTo
});
+ props.setDays([...props.days]);
+
return;
}
@@ -251,27 +108,31 @@ const AvailabilityPicker = (props: {
if (existingTimeContainingFrom >= 0 && existingTimeContainingTo < 0) {
let newFrom = day.availableTimes[existingTimeContainingFrom].fromTime;
- day.availableTimes.splice(existingTimeContainingFrom);
+ day.availableTimes.splice(existingTimeContainingFrom, 1);
day.availableTimes.push({
fromTime: newFrom,
toTime: toTime
});
+ props.setDays([...props.days]);
+
return;
}
// The newly created availability to is within an existing one. Combine the 2 into one.
- if (existingTimeContainingFrom >= 0 && existingTimeContainingTo < 0) {
- let newTo = day.availableTimes[existingTimeContainingFrom].toTime;
+ if (existingTimeContainingFrom < 0 && existingTimeContainingTo >= 0) {
+ let newTo = day.availableTimes[existingTimeContainingTo].toTime;
- day.availableTimes.splice(existingTimeContainingFrom);
+ day.availableTimes.splice(existingTimeContainingTo, 1);
day.availableTimes.push({
fromTime: fromTime,
toTime: newTo
});
+ props.setDays([...props.days]);
+
return;
}
@@ -280,12 +141,13 @@ const AvailabilityPicker = (props: {
toTime: toTime
});
- setDays([...days])
+ props.setDays([...props.days]);
}
return (
e.preventDefault()}
>
setGhostPreviewProps(null)}
>
{
- days.map(day =>
-
+ {
+ return {
+ userName: a.userName,
+ availableTimes: a.days.find(d => d.forDate.unix() === day.forDate.unix())?.availableTimes ?? []
+ } as OthersDay;
+ })}
onMouseEnterHalfhour={(e: MouseEvent, time: dayjs.Dayjs) => {
displayGhostPeriod(e, time);
}}
- onMouseClickHalfhour={(day: AvailabilityDay, time: dayjs.Dayjs) => {
- createAvailability(day, time);
+ onMouseClickHalfhour={(day: AvailabilityDay, time: dayjs.Dayjs, isDelete: boolean) => {
+ changeAvailability(day, time, isDelete);
}}
/>
)
diff --git a/frontend/src/components/AvailabilityPickerDay.tsx b/frontend/src/components/AvailabilityPickerDay.tsx
new file mode 100644
index 0000000..0bc9eae
--- /dev/null
+++ b/frontend/src/components/AvailabilityPickerDay.tsx
@@ -0,0 +1,93 @@
+import { AvailabilityDay, OthersDay } from "../types/Availabilities";
+import utils from "../utils";
+import { Box, Card, Divider, Typography } from "@mui/material";
+import { EventTypes } from "../types/Event";
+import AvailabilityPickerHour from "./AvailabilityPickerHour";
+import "./css/AvailabilityPicker.css";
+import dayjs from "dayjs";
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+dayjs.extend(localizedFormat)
+
+const AvailabilityPickerDay = (props: {
+ day: AvailabilityDay,
+ eventType: String,
+ halfHourDisplayHeight: number,
+ othersAvailabilityDay: OthersDay[],
+ onMouseEnterHalfhour: (e: React.MouseEvent, time: dayjs.Dayjs) => void,
+ onMouseClickHalfhour: (day: AvailabilityDay, time: dayjs.Dayjs, isDelete: boolean) => void
+}) => {
+
+ const generateHours = (): JSX.Element[] => {
+ let hours: JSX.Element[] = [];
+
+ for (var i = 0; i < 24; i++) {
+ let fullHourTime = props.day.forDate.set("hour", i);
+ let halfHourTime = fullHourTime.add(30, "minutes");
+
+ hours.push(
+ d.availableTimes.some(t => utils.dayjsIsBetweenUnixExclusive(t.fromTime, fullHourTime, t.toTime))).map(d => d.userName)}
+ namesMarkedHalfHourAsAvailable={props.othersAvailabilityDay.filter(d => d.availableTimes.some(t => utils.dayjsIsBetweenUnixExclusive(t.fromTime, halfHourTime, t.toTime))).map(d => d.userName)}
+ isFullHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, fullHourTime, a.toTime))}
+ isHalfHourSelected={props.day.availableTimes.some(a => utils.dayjsIsBetweenUnixExclusive(a.fromTime, halfHourTime, a.toTime))}
+ onMouseEnterHalfhour={(e: React.MouseEvent, time: dayjs.Dayjs): void => {
+ props.onMouseEnterHalfhour(e, time);
+ }}
+ onMouseClickOnHalfhour={(time: dayjs.Dayjs, isDelete: boolean): void => {
+ props.onMouseClickHalfhour(props.day, time, isDelete);
+ }}
+ />
+ );
+ }
+
+ return hours;
+ }
+
+ return (
+
+
+ {
+ (props.eventType === EventTypes.WEEK) &&
+
+ { props.day.forDate.format("dddd") }
+
+ }
+ {
+ (props.eventType === EventTypes.DAY) &&
+
+ Any Day
+
+ }
+ {
+ (props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) &&
+
+ { props.day.forDate.format("LL") }
+
+ }
+
+
+
+
+ {generateHours()}
+
+
+ );
+}
+
+export default AvailabilityPickerDay;
\ No newline at end of file
diff --git a/frontend/src/components/AvailabilityPickerHour.tsx b/frontend/src/components/AvailabilityPickerHour.tsx
new file mode 100644
index 0000000..28a1135
--- /dev/null
+++ b/frontend/src/components/AvailabilityPickerHour.tsx
@@ -0,0 +1,86 @@
+import { Box, Typography } from "@mui/material";
+import classNames from "classnames";
+import utils from "../utils";
+import "./css/AvailabilityPicker.css";
+import dayjs, { Dayjs } from "dayjs";
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import DisableableTooltip from "./DisableableTooltip";
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+dayjs.extend(localizedFormat)
+
+const AvailabilityPickerHour = (props: {
+ dateTime: Dayjs,
+ isFullHourSelected: boolean,
+ isHalfHourSelected: boolean,
+ halfHourDisplayHeight: number,
+ namesMarkedFullHourAsAvailable: String[],
+ namesMarkedHalfHourAsAvailable: String[],
+ onMouseEnterHalfhour: (e: React.MouseEvent, time: Dayjs) => void,
+ onMouseClickOnHalfhour: (time: Dayjs, isDelete: boolean) => void
+}) => {
+ const generateTooltipText = (names: String[]): String => {
+ return `${names.length} ${names.length > 1 ? "people have" : "person has"} marked this time as available: ${names.join(", ")}`;
+ }
+
+ return (
+
+
+ props.onMouseEnterHalfhour(e, props.dateTime)}
+ onMouseDown={(e) => {
+ if (e.button !== 0 && e.button !== 2) {
+ return;
+ }
+
+ props.onMouseClickOnHalfhour(props.dateTime, e.button === 2);
+ }}
+ >
+
+ { utils.formatTimeFromHourOfDay(props.dateTime.hour(), 0) }
+
+
+
+
+ props.onMouseEnterHalfhour(e, props.dateTime.add(30, "minutes"))}
+ onMouseDown={(e) => {
+ if (e.button !== 0 && e.button !== 2) {
+ return;
+ }
+
+ props.onMouseClickOnHalfhour(props.dateTime.add(30, "minutes"), e.button === 2);
+ }}
+ />
+
+
+ );
+}
+
+export default AvailabilityPickerHour;
\ No newline at end of file
diff --git a/frontend/src/components/DisableableTooltip.tsx b/frontend/src/components/DisableableTooltip.tsx
new file mode 100644
index 0000000..69e8bdb
--- /dev/null
+++ b/frontend/src/components/DisableableTooltip.tsx
@@ -0,0 +1,28 @@
+import { useState, useEffect } from 'react'
+
+import { Tooltip, TooltipProps } from '@mui/material'
+
+const DisableableTooltip = ({
+ disabled,
+ children,
+ ...tooltipProps
+}: TooltipProps & { disabled: boolean }) => {
+ const [open, setOpen] = useState(false)
+
+ useEffect(() => {
+ if (disabled) setOpen(false)
+ }, [disabled])
+
+ return (
+ !disabled && setOpen(true)}
+ onClose={() => setOpen(false)}
+ >
+ {children}
+
+ )
+}
+
+export default DisableableTooltip;
\ No newline at end of file
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
index 47e7dff..4dbe604 100644
--- a/frontend/src/components/Footer.tsx
+++ b/frontend/src/components/Footer.tsx
@@ -7,7 +7,7 @@ export default function Footer() {
direction="column"
justifyContent="center"
>
-
+
Created by mvvasilev | Github
diff --git a/frontend/src/components/css/AvailabilityPicker.css b/frontend/src/components/css/AvailabilityPicker.css
index f108096..e6a92e2 100644
--- a/frontend/src/components/css/AvailabilityPicker.css
+++ b/frontend/src/components/css/AvailabilityPicker.css
@@ -1,24 +1,74 @@
:root {
- --hover-color: #004455;
- --hour-light-color: #003344;
- --hour-dark-color: #002233;
+ --hover-color: #555555;
+ --hour-light-color: #333333;
+ --hour-dark-color: #222222;
--hour-border-bolor: #777;
--hour-text-color: #ddd;
--active-color: #223300;
- --currently-selected-color: #112200;
+ --currently-selected-color: #336622;
+ --currently-selected-color-hover: #338822;
--halfhour-border-color: #333;
+ --halfhour-border-color-light: #777;
+ --ghost-box-color: #118811;
+ --time-text-color: #ddd;
+ --hour-marked-available: #330033;
+
+}
+
+div[class*=" hour-available-"]{
+ background-color: #bb00bb;
+}
+
+div.hour-available-0 {
+ background-color: inherit;
+}
+
+div.hour-available-1 {
+ background-color: #330033;
+}
+
+div.hour-available-2 {
+ background-color: #440044;
+}
+
+div.hour-available-3 {
+ background-color: #550055;
+}
+
+div.hour-available-4 {
+ background-color: #660066;
+}
+
+div.hour-available-5 {
+ background-color: #770077;
+}
+
+div.hour-available-6 {
+ background-color: #880088;
+}
+
+div.hour-available-7 {
+ background-color: #990099;
+}
+
+div.hour-available-8 {
+ background-color: #aa00aa;
+}
+
+div.hour-available-9 {
+ background-color: #bb00bb;
}
div.hour-light {
width: 100%;
- border-bottom: solid 1px;
+ border-top: solid 1px;
border-color: var(--hour-border-bolor);
background-color: var(--hour-light-color);
}
div.hour-dark {
width: 100%;
- border-bottom: solid 1px;
+ border-top: solid 1px;
border-color: var(--hour-border-bolor);
background-color: var(--hour-dark-color);
}
@@ -26,7 +76,7 @@ div.hour-dark {
div.full-hour {
width: 100%;
border-bottom: dashed 1px;
- border-color: var(--halfhour-border-color)
+ border-color: var(--halfhour-border-color-light)
}
div.half-hour {
@@ -45,12 +95,16 @@ div.full-hour:active, div.half-hour:active {
background-color: var(--active-color);
}
+div.selected-availability:hover {
+ background-color: var(--currently-selected-color-hover);
+}
+
div.ghost-box {
position: absolute;
- background-color: rgba(0, 255, 0, 0.1);
+ background-color: rgba(0, 255, 0, 0.2);
border: solid 1px;
- border-color: #227722;
- border-radius: 1;
+ border-color: var(--ghost-box-color);
+ border-radius: 4%;
margin: 0;
padding: 0;
pointer-events: none;
@@ -59,7 +113,7 @@ div.ghost-box {
p.time-text {
text-align: left;
font-size: 0.65em;
- color: #ddd;
+ color: var(--time-text-color);
}
div.day-card {
diff --git a/frontend/src/pages/ExistingEventPage.tsx b/frontend/src/pages/ExistingEventPage.tsx
index 26310a2..e19a47e 100644
--- a/frontend/src/pages/ExistingEventPage.tsx
+++ b/frontend/src/pages/ExistingEventPage.tsx
@@ -1,31 +1,180 @@
import { useEffect, useState } from "react";
-import { useParams } from "react-router-dom";
-import { Event, createEvent } from '../types/Event';
+import { useNavigate, useParams } from "react-router-dom";
+import { Event, EventTypes, createEvent } from '../types/Event';
import Grid from '@mui/material/Unstable_Grid2'
import { Button, TextField, Typography } from "@mui/material";
import AvailabilityPicker from "../components/AvailabilityPicker";
-import dayjs from "dayjs";
+import dayjs, { Dayjs } from "dayjs";
import utils from "../utils";
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import duration from 'dayjs/plugin/duration';
+import { AvailabilityDay, AvailabilityTime, OthersDays } from "../types/Availabilities";
+import toast from "react-hot-toast";
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+dayjs.extend(localizedFormat)
+dayjs.extend(duration);
export default function ExistingEventPage() {
+ const navigate = useNavigate();
+
let { eventId } = useParams();
+ const [canSubmit, setCanSubmit] = useState(false);
const [event, setEvent] = useState(createEvent());
+ const [days, setDays] = useState([]);
+ const [othersDays, setOthersDays] = useState([]);
+ const [userName, setUserName] = useState(undefined);
useEffect(() => {
- fetch(`/api/events/${eventId}`)
- .then(resp => resp.json())
- .then(resp => setEvent({
- name: resp.result?.name,
- description: resp.result?.description,
- fromDate: dayjs.utc(resp.result?.from_date),
- toDate: dayjs.utc(resp.result?.to_date),
- eventType: resp.result?.event_type,
- snowflakeId: resp.result?.snowflake_id,
- duration: resp.result?.duration
- }));
+ utils.showSpinner();
+
+ Promise.all([
+ utils.performRequest(`/api/events/${eventId}`)
+ .then(result => setEvent({
+ name: result?.name,
+ description: result?.description,
+ fromDate: dayjs.utc(result?.from_date),
+ toDate: dayjs.utc(result?.to_date),
+ eventType: result?.event_type,
+ snowflakeId: result?.snowflake_id,
+ duration: result?.duration
+ }))
+ .catch(e => toast.error(e)),
+
+ utils.performRequest(`/api/events/${eventId}/availabilities`)
+ .then((result: [{ id: number, from_date: string, to_date: string, user_name: string }]) => {
+ let othersDays: OthersDays[] = [];
+ let localTimezone = dayjs.tz.guess();
+
+ for (const availability of result) {
+ let fromDate = dayjs(availability.from_date).tz(localTimezone);
+ let toDate = dayjs(availability.to_date).tz(localTimezone);
+
+ var userTimes = othersDays.find(d => d.userName === availability.user_name);
+
+ if (!userTimes) {
+ userTimes = { userName: availability.user_name, days: []} as OthersDays;
+
+ othersDays.push(userTimes);
+ }
+
+ let availabilityDay = fromDate.hour(0).minute(0).second(0).millisecond(0);
+
+ var day = userTimes.days.find(d => d.forDate.unix() === availabilityDay.unix());
+
+ if (!day) {
+ day = {
+ forDate: availabilityDay,
+ availableTimes: []
+ } as AvailabilityDay;
+
+ userTimes.days.push(day);
+ }
+
+ day.availableTimes.push({
+ fromTime: fromDate,
+ toTime: toDate
+ } as AvailabilityTime)
+ }
+
+ setOthersDays(othersDays);
+ })
+ .catch(e => toast.error(e))
+ ])
+ .finally(() => utils.hideSpinner());;
+
}, [eventId]);
+ useEffect(() => {
+ document.title = `findtheti.me - ${event.name}`;
+ }, [event])
+
+ useEffect(() => {
+ if (event.fromDate === null || event.toDate === null || event.eventType === null) {
+ return;
+ }
+
+ let localTimezone = dayjs.tz.guess();
+
+ let localFromDate = event.fromDate.tz(localTimezone);
+ let localToDate = event.toDate.tz(localTimezone);
+
+ switch (event.eventType) {
+ case EventTypes.SPECIFIC_DATE: {
+ createAvailabilitiesBasedOnInitialDate(localFromDate, 1);
+ break;
+ }
+ case EventTypes.DATE_RANGE: {
+ createAvailabilitiesBasedOnInitialDate(localFromDate, Math.abs(localFromDate.diff(localToDate, "day", false)));
+ break;
+ }
+ case EventTypes.DAY: {
+ createAvailabilitiesBasedOnUnspecifiedInitialDate(1, localTimezone);
+ break;
+ }
+ case EventTypes.WEEK: {
+ createAvailabilitiesBasedOnUnspecifiedInitialDate(7, localTimezone);
+ break;
+ }
+ }
+ }, [event]);
+
+ useEffect(() => {
+ var valid = !utils.isNullOrUndefined(userName) && userName !== "";
+
+ valid &&= days.some(day => day.availableTimes.length > 0);
+
+ setCanSubmit(valid);
+ }, [userName, days]);
+
+ const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string) => {
+ createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays);
+ }
+
+ const createAvailabilitiesBasedOnInitialDate = (date: Dayjs, numberOfDays: number) => {
+ let availabilities: AvailabilityDay[] = [];
+
+ for (var i: number = 0; i < numberOfDays; i++) {
+ let availability: AvailabilityDay = {
+ forDate: date.add(i, "day").startOf("day"),
+ availableTimes: []
+ }
+
+ availabilities.push(availability);
+ }
+
+ setDays(availabilities);
+ }
+
+ const submitAvailabilities = () => {
+ utils.showSpinner();
+
+ let body = {
+ user_name: userName,
+ availabilities: days.flatMap(day => day.availableTimes.map(a => {
+ return {
+ from_date: a.fromTime.utc(),
+ to_date: a.toTime.utc()
+ }
+ }))
+ };
+
+ utils.performRequest(`/api/events/${eventId}/availabilities`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(body)
+ })
+ .then(_ => navigate("/thank-you"))
+ .catch(e => toast.error(e))
+ .finally(() => utils.hideSpinner());
+ };
+
return (
@@ -49,29 +198,41 @@ export default function ExistingEventPage() {
{
(event.fromDate !== null && event.toDate !== null && event.eventType !== null) &&
setDays(days)}
+ othersAvailabilities={othersDays}
eventType={event.eventType}
availabilityDurationInMinutes={event.duration}
/>
}
+
+ Left-click to select when you're available, right-click to remove the highlighted hours.
+
{
- // event.description = e.target.value;
- // setEvent({...event});
- // }}
+ value={userName || ""}
+ onChange={(e) => {
+ if (e.target.value?.length > 100) {
+ e.preventDefault();
+ return;
+ }
+
+ setUserName(e.target.value);
+ }}
label="Your Name"
/>
-
diff --git a/frontend/src/pages/NewEventPage.tsx b/frontend/src/pages/NewEventPage.tsx
index 3c4dd6c..edb6a33 100644
--- a/frontend/src/pages/NewEventPage.tsx
+++ b/frontend/src/pages/NewEventPage.tsx
@@ -1,10 +1,12 @@
import { Alert, Button, MenuItem, Select, Slider, TextField, Typography } from '@mui/material';
import Grid from '@mui/material/Unstable_Grid2'
-import { DateTimePicker } from '@mui/x-date-pickers';
+import { DatePicker } from '@mui/x-date-pickers';
import { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom"
import { Event, EventTypes, createEvent } from '../types/Event';
import utils from '../utils';
+import toast from 'react-hot-toast';
+import dayjs from 'dayjs';
export default function NewEventPage() {
const navigate = useNavigate();
@@ -17,26 +19,42 @@ export default function NewEventPage() {
}, [event])
function validateEvent(): void {
- console.log(event);
var valid: boolean = true;
- valid &&= event.name && event.name !== "";
- valid &&= event.eventType !== EventTypes.UNKNOWN || event.eventType !== null;
+ let today = dayjs().hour(0).minute(0).second(0).millisecond(0);
+
+ valid &&= !utils.isNullOrUndefined(event.name) && event.name !== "";
+
+ valid &&= event.eventType !== EventTypes.UNKNOWN && event.eventType !== null;
if (event.eventType === EventTypes.DATE_RANGE) {
- valid &&= event.fromDate !== null;
- valid &&= event.toDate !== null;
+
+ valid &&= !utils.isNullOrUndefined(event.fromDate) && !utils.isNullOrUndefined(event.toDate);
+
+ valid &&= !utils.isNullOrUndefined(event.toDate) && event.toDate!.unix() > today.unix();
+
+ valid &&= !utils.isNullOrUndefined(event.fromDate) && event.fromDate!.unix() >= today.unix();
+
+ valid &&= !utils.isNullOrUndefined(event.fromDate) && !utils.isNullOrUndefined(event.toDate) && event.toDate!.unix() > event.fromDate!.unix();
+
+ valid &&= !utils.isNullOrUndefined(event.fromDate) && !utils.isNullOrUndefined(event.toDate) && event.toDate!.diff(event.fromDate!, "days") >= 1;
+
+ valid &&= !utils.isNullOrUndefined(event.fromDate) && !utils.isNullOrUndefined(event.toDate) && event.toDate!.diff(event.fromDate!, "days") <= 14;
}
if (event.eventType === EventTypes.SPECIFIC_DATE) {
- valid &&= event.fromDate !== null;
+ valid &&= !utils.isNullOrUndefined(event.fromDate);
+
+ valid &&= !utils.isNullOrUndefined(event.fromDate) && event.fromDate!.unix() >= today.unix();
}
setEventValid(valid);
}
function saveEvent() {
- fetch("/api/events", {
+ utils.showSpinner();
+
+ utils.performRequest("/api/events", {
method: "POST",
headers: {
"Content-Type": "application/json"
@@ -50,10 +68,13 @@ export default function NewEventPage() {
duration: event.duration
})
})
- .then(resp => resp.json())
.then(resp => {
- navigate(resp.result.snowflake_id)
+ navigate(resp.snowflake_id)
})
+ .catch(err => {
+ toast.error(err)
+ })
+ .finally(() => utils.hideSpinner());
}
return (
@@ -146,8 +167,9 @@ export default function NewEventPage() {
{
event.eventType == EventTypes.SPECIFIC_DATE &&
- {
event.fromDate = value ?? null;
@@ -160,8 +182,9 @@ export default function NewEventPage() {
{
event.eventType == EventTypes.DATE_RANGE &&
- {
event.fromDate = value ?? null;
@@ -174,8 +197,10 @@ export default function NewEventPage() {
{
event.eventType == EventTypes.DATE_RANGE &&
- {
event.toDate = value ?? null;
diff --git a/frontend/src/pages/RootLayout.tsx b/frontend/src/pages/RootLayout.tsx
index 01e6a7c..be1db97 100644
--- a/frontend/src/pages/RootLayout.tsx
+++ b/frontend/src/pages/RootLayout.tsx
@@ -1,5 +1,5 @@
import { ThemeProvider } from "@emotion/react";
-import { CssBaseline, Divider, Paper, createTheme } from "@mui/material";
+import { Backdrop, CircularProgress, CssBaseline, Divider, Paper, createTheme } from "@mui/material";
import Grid from '@mui/material/Unstable_Grid2'
import GithubCorner from "react-github-corner";
import Header from "../components/Header";
@@ -7,7 +7,10 @@ import Footer from "../components/Footer";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import dayjs from "dayjs";
-import * as utc from 'dayjs/plugin/utc';
+import utc from 'dayjs/plugin/utc';
+import { Toaster } from "react-hot-toast";
+import { useEffect, useState } from "react";
+import utils from "../utils";
dayjs.extend(utc);
@@ -17,11 +20,39 @@ const theme = createTheme({
}
});
-export default function RootLayout(props: { children: React.ReactNode }) {
+const RootLayout = (props: { children: React.ReactNode }) => {
+
+ const [spinner, showSpinner] = useState(false);
+
+ useEffect(() => {
+ window.addEventListener("onSpinnerStatusChange", () => {
+ showSpinner(utils.isSpinnerShown());
+ });
+ }, []);
+
return (
+ {
+
+
+
+ }
+
+
+
);
-};
\ No newline at end of file
+};
+
+export default RootLayout;
\ No newline at end of file
diff --git a/frontend/src/pages/ThankYouPage.tsx b/frontend/src/pages/ThankYouPage.tsx
new file mode 100644
index 0000000..401cace
--- /dev/null
+++ b/frontend/src/pages/ThankYouPage.tsx
@@ -0,0 +1,23 @@
+import { Typography } from '@mui/material';
+import Grid from '@mui/material/Unstable_Grid2'
+import { useNavigate } from 'react-router-dom';
+
+const ThankYouPage = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+ Thank You!
+
+
+ Your response has been recorded. Check in with the event organizer(s) for the exact date and time!
+
+
+ To view the available times of all attendees, feel free to navigate(-1)}>navigate back to the event page.
+
+
+ );
+}
+
+export default ThankYouPage;
\ No newline at end of file
diff --git a/frontend/src/types/Availabilities.tsx b/frontend/src/types/Availabilities.tsx
new file mode 100644
index 0000000..1438ea3
--- /dev/null
+++ b/frontend/src/types/Availabilities.tsx
@@ -0,0 +1,21 @@
+import { Dayjs } from "dayjs"
+
+export type AvailabilityTime = {
+ fromTime: Dayjs,
+ toTime: Dayjs
+}
+
+export type AvailabilityDay = {
+ forDate: Dayjs,
+ availableTimes: AvailabilityTime[]
+}
+
+export type OthersDays = {
+ userName: String,
+ days: AvailabilityDay[]
+}
+
+export type OthersDay = {
+ userName: String,
+ availableTimes: AvailabilityTime[]
+}
\ No newline at end of file
diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts
index e10c8c9..a1fe5bb 100644
--- a/frontend/src/utils.ts
+++ b/frontend/src/utils.ts
@@ -1,9 +1,43 @@
-import dayjs from "dayjs";
-import * as duration from 'dayjs/plugin/duration';
-
-dayjs.extend(duration)
+import dayjs, { Dayjs } from "dayjs";
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import duration from 'dayjs/plugin/duration';
+dayjs.extend(utc)
+dayjs.extend(timezone)
+dayjs.extend(localizedFormat)
+dayjs.extend(duration);
const utils = {
+ performRequest: (url: string | URL | Request, options?: RequestInit | undefined): Promise => {
+ return fetch(url, options).then(async resp => {
+ if (!resp.ok) {
+ let errorTextResult = await resp.text();
+
+ var errorMsg = errorTextResult;
+
+ try {
+ let jsonResult: any = JSON.parse(errorTextResult);
+
+ errorMsg = jsonResult?.error?.message;
+ } catch(err) {
+ errorMsg = errorTextResult;
+ }
+
+ throw errorMsg;
+ } else {
+ let successTextResult = await resp.text();
+
+ try {
+ let jsonResult = JSON.parse(successTextResult);
+
+ return jsonResult?.result;
+ } catch(err) {
+ return successTextResult;
+ }
+ }
+ })
+ },
toHoursAndMinutes: (totalMinutes: number): { hours: number, minutes: number } => {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
@@ -24,6 +58,26 @@ const utils = {
},
formatTimeFromHourOfDay: (hourOfDay: number, minutes: number): String => {
return dayjs.duration({ hours: hourOfDay, minutes: minutes }).format('HH:mm');
+ },
+ dayjsIsBetweenUnixExclusive: (lowerBound: Dayjs, time: Dayjs, upperBound: Dayjs): boolean => {
+ return lowerBound.unix() <= time.unix() && time.unix() < upperBound.unix()
+ },
+ dayjsIsBetweenUnixInclusive: (lowerBound: Dayjs, time: Dayjs, upperBound: Dayjs): boolean => {
+ return lowerBound.unix() <= time.unix() && time.unix() <= upperBound.unix()
+ },
+ isSpinnerShown: (): boolean => {
+ return localStorage.getItem("SpinnerShowing") === "true";
+ },
+ showSpinner: (): void => {
+ localStorage.setItem("SpinnerShowing", "true");
+ window.dispatchEvent(new Event("onSpinnerStatusChange"));
+ },
+ hideSpinner: (): void => {
+ localStorage.removeItem("SpinnerShowing");
+ window.dispatchEvent(new Event("onSpinnerStatusChange"));
+ },
+ isNullOrUndefined: (thing: any): boolean => {
+ return thing === null || thing === undefined;
}
}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index a7fc6fb..c19291e 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -5,6 +5,7 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
+ "esModuleInterop": true,
/* Bundler mode */
"moduleResolution": "bundler",
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 6924b21..87677a2 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1487,6 +1487,11 @@ globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
+goober@^2.1.10:
+ version "2.1.13"
+ resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c"
+ integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==
+
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz"
@@ -1864,6 +1869,13 @@ react-github-corner@^2.5.0:
resolved "https://registry.yarnpkg.com/react-github-corner/-/react-github-corner-2.5.0.tgz#e350d0c69f69c075bc0f1d2a6f1df6ee91da31f2"
integrity sha512-ofds9l6n61LJc6ML+jSE6W9ZSQvATcMR9evnHPXua1oDYj289HKODnVqFUB/g2a4ieBjDHw416iHP3MjqnU76Q==
+react-hot-toast@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994"
+ integrity sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==
+ dependencies:
+ goober "^2.1.10"
+
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
diff --git a/src/api.rs b/src/api.rs
index 5dab7a7..cf621c1 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -31,18 +31,26 @@ pub(crate) async fn routes() -> Result {
.with_state(AppState::new().await?))
}
-pub(crate) fn ok(r: Result) -> UniversalResponseDto {
+pub(crate) fn ok(r: Result) -> UniversalResponseDto {
match r {
Ok(res) => UniversalResponseDto {
status: StatusCode::OK,
result: Some(res),
error: None,
},
- Err(err) => internal_server_error(err),
+ Err(err) => error(err),
}
}
-pub(crate) fn internal_server_error(e: E) -> UniversalResponseDto {
+pub(crate) fn error(e: ApplicationError) -> UniversalResponseDto {
+ UniversalResponseDto {
+ status: e.status,
+ result: None,
+ error: Some(ErrorDto { message: format!("{}", e)})
+ }
+}
+
+pub(crate) fn internal_server_error(e: ApplicationError) -> UniversalResponseDto {
UniversalResponseDto {
status: StatusCode::INTERNAL_SERVER_ERROR,
result: None,
@@ -104,14 +112,15 @@ impl AppState {
}
}
-#[derive(Debug, Serialize)]
+#[derive(Debug)]
pub struct ApplicationError {
+ status: StatusCode,
msg: String,
}
impl ApplicationError {
- pub fn new(msg: String) -> Self {
- Self { msg }
+ pub fn new(msg: String, status: StatusCode) -> Self {
+ Self { msg, status }
}
}
@@ -119,6 +128,7 @@ impl From for ApplicationError {
fn from(value: sqlx::Error) -> Self {
Self {
msg: value.to_string(),
+ status: StatusCode::INTERNAL_SERVER_ERROR
}
}
}
@@ -127,6 +137,7 @@ impl From for ApplicationError {
fn from(value: MigrateError) -> Self {
Self {
msg: value.to_string(),
+ status: StatusCode::INTERNAL_SERVER_ERROR
}
}
}
@@ -135,6 +146,7 @@ impl From for ApplicationError {
fn from(value: VarError) -> Self {
Self {
msg: value.to_string(),
+ status: StatusCode::INTERNAL_SERVER_ERROR
}
}
}
diff --git a/src/endpoints.rs b/src/endpoints.rs
index 320f9d2..3d34020 100644
--- a/src/endpoints.rs
+++ b/src/endpoints.rs
@@ -1,6 +1,8 @@
+use std::net::SocketAddr;
+
use axum::{
- extract::{Path, State},
- Json,
+ extract::{Path, State, ConnectInfo},
+ Json, http::StatusCode, Extension,
};
use chrono::{DateTime, TimeZone, Utc};
use rand::{distributions::Alphanumeric, Rng};
@@ -13,7 +15,7 @@ use crate::{
entity::{
availability::Availability,
event::{Event, EventType},
- },
+ }
};
#[derive(Deserialize)]
@@ -41,7 +43,6 @@ pub struct EventDto {
pub struct CreateAvailabilitiesDto {
availabilities: Vec,
user_email: Option,
- user_ip: String,
user_name: String,
}
@@ -67,7 +68,7 @@ pub async fn create_event(
let mut conn = match app_state.db_pool.acquire().await {
Ok(c) => c,
- Err(e) => return api::internal_server_error(e),
+ Err(e) => return api::internal_server_error(e.into()),
};
let res = conn
@@ -82,9 +83,31 @@ pub async fn create_event(
let event_type: EventType = dto.event_type.into();
if matches!(event_type, EventType::Unknown) {
- return Err(ApplicationError::new(
- "Unknown event type, invalid variant.".to_string(),
- ));
+ return Err(ApplicationError::new("Unknown event type, invalid variant.".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
+ }
+
+ if matches!(event_type, EventType::SpecificDate) && dto.from_date.is_none() {
+ return Err(ApplicationError::new("SpecificDate event type supplied, but missing from_date".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
+ }
+
+ if matches!(event_type, EventType::DateRange) {
+ match (dto.from_date, dto.to_date) {
+ (Some(from), Some(to)) => {
+ if from >= to {
+ return Err(ApplicationError::new("Supplied from_date is later than or equal to to_date".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
+ }
+
+ if (to - from).num_days() < 1 {
+ return Err(ApplicationError::new("Difference between from_date and to_date is less than 1 day".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
+ }
+
+ if (to - from).num_days() > 14 {
+ return Err(ApplicationError::new("Difference between from_date and to_date is greater than 14 days ( current supported maximum )".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
+ }
+ },
+ _ => return Err(ApplicationError::new("DateRange event type supplied, but missing either from_date or to_date".to_string(), StatusCode::UNPROCESSABLE_ENTITY))
+ }
+
}
let event_id = db::insert_event_and_fetch_id(
@@ -117,7 +140,7 @@ pub async fn create_event(
})
.await;
- api::ok::(res)
+ api::ok::(res)
}
pub async fn fetch_event(
@@ -126,7 +149,7 @@ pub async fn fetch_event(
) -> UniversalResponseDto {
let mut conn = match app_state.db_pool.acquire().await {
Ok(c) => c,
- Err(e) => return api::internal_server_error(e),
+ Err(e) => return api::internal_server_error(e.into()),
};
let res = conn
@@ -147,35 +170,39 @@ pub async fn fetch_event(
})
.await;
- api::ok::(res)
+ api::ok::(res)
}
pub async fn create_availabilities(
State(app_state): State,
Path(event_snowflake_id): Path,
+ ConnectInfo(addr): ConnectInfo,
Json(dto): Json,
) -> UniversalResponseDto<()> {
let mut conn = match app_state.db_pool.acquire().await {
Ok(c) => c,
- Err(e) => return api::internal_server_error(e),
+ Err(e) => return api::internal_server_error(e.into()),
};
let res = conn
.transaction(|txn| {
Box::pin(async move {
+ let user_ip = format!("{}", addr.ip());
+
let event = db::fetch_event_by_snowflake_id(txn, event_snowflake_id).await?;
let current_availabilities = db::fetch_event_availabilities(txn, event.id).await?;
let already_submitted = current_availabilities.iter().any(|a| {
- (dto.user_email.is_none() && a.user_email == dto.user_email)
- || a.user_ip == dto.user_ip
+ (dto.user_email.is_some() && a.user_email.is_some() && a.user_email == dto.user_email)
+ || a.user_ip == user_ip
|| a.user_name == dto.user_name
});
if already_submitted {
return Err(ApplicationError::new(
"Availability already submitted".to_string(),
+ StatusCode::UNPROCESSABLE_ENTITY
));
}
@@ -190,7 +217,7 @@ pub async fn create_availabilities(
from_date: a.from_date.naive_utc(),
to_date: a.to_date.naive_utc(),
user_email: dto.user_email.clone(),
- user_ip: dto.user_ip.clone(),
+ user_ip: user_ip.clone(),
user_name: dto.user_name.clone(),
},
)
@@ -202,7 +229,7 @@ pub async fn create_availabilities(
})
.await;
- api::ok::<(), ApplicationError>(res)
+ api::ok::<()>(res)
}
pub async fn fetch_availabilities(
@@ -211,7 +238,7 @@ pub async fn fetch_availabilities(
) -> UniversalResponseDto> {
let mut conn = match app_state.db_pool.acquire().await {
Ok(c) => c,
- Err(e) => return api::internal_server_error(e),
+ Err(e) => return api::internal_server_error(e.into()),
};
let res = conn
@@ -234,5 +261,5 @@ pub async fn fetch_availabilities(
})
.await;
- api::ok::, ApplicationError>(res)
+ api::ok::>(res)
}
diff --git a/src/main.rs b/src/main.rs
index 4151920..89070d0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -30,7 +30,7 @@ async fn main() {
let listener = TcpListener::bind(addr).await.unwrap();
- axum::serve(listener, routes.into_make_service())
+ axum::serve(listener, routes.into_make_service_with_connect_info::())
.await
.unwrap();
}
\ No newline at end of file