From 0dcf39b48a3fb76dac603764a518113750251d6b Mon Sep 17 00:00:00 2001 From: mvvasilev <me@mvvasilev.dev> Date: Tue, 9 Jan 2024 17:23:40 +0200 Subject: [PATCH] First try at availability picker --- frontend/package.json | 3 + frontend/src/App.css | 12 + .../src/components/AvailabilityPicker.tsx | 308 ++++++++++++++++++ frontend/src/components/Footer.tsx | 15 + frontend/src/components/Header.tsx | 30 ++ frontend/src/pages/ExistingEventPage.tsx | 77 ++++- frontend/src/pages/NewEventPage.tsx | 209 +++++++++++- frontend/src/pages/RootLayout.tsx | 50 ++- frontend/src/types/Event.tsx | 32 ++ frontend/src/utils.ts | 30 ++ frontend/vite.config.ts | 9 + frontend/yarn.lock | 258 +++++++++++++-- .../20240108212119_EventSetDatesOptional.sql | 11 + .../20240109082813_EventAddDuration.sql | 2 + src/db.rs | 7 +- src/endpoints.rs | 25 +- src/entity/event.rs | 5 +- src/main.rs | 13 +- 18 files changed, 1038 insertions(+), 58 deletions(-) create mode 100644 frontend/src/components/AvailabilityPicker.tsx create mode 100644 frontend/src/components/Footer.tsx create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/types/Event.tsx create mode 100644 frontend/src/utils.ts create mode 100644 migrations/20240108212119_EventSetDatesOptional.sql create mode 100644 migrations/20240109082813_EventAddDuration.sql diff --git a/frontend/package.json b/frontend/package.json index 01adcad..2a2ea8e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,8 +13,11 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.3", + "@mui/x-date-pickers": "^6.18.7", + "dayjs": "^1.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-github-corner": "^2.5.0", "react-router-dom": "^6.21.1" }, "devDependencies": { diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..6b208be 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,3 +1,15 @@ +@import url('https://fonts.googleapis.com/css2?family=Bungee+Spice&display=swap'); + +.noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome, Edge, Opera and Firefox */ +} + #root { max-width: 1280px; margin: 0 auto; diff --git a/frontend/src/components/AvailabilityPicker.tsx b/frontend/src/components/AvailabilityPicker.tsx new file mode 100644 index 0000000..0b1554f --- /dev/null +++ b/frontend/src/components/AvailabilityPicker.tsx @@ -0,0 +1,308 @@ +import { Box, Card, Divider, Stack, Typography } from "@mui/material"; +import dayjs, { Dayjs } from "dayjs"; +import { 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"; + +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; + +const DAY_DISPLAY_WIDTH: String = "150px"; + +export default function AvailabilityPicker(props: { + fromDate: Dayjs, + toDate: Dayjs, + eventType: String, + availabilityDurationInMinutes: number +}) { + const [days, setDays] = useState<AvailabilityDay[]>([]); + const [selectingAvailabilityForDay, setAvailabilityDayBeingSelectedFor] = useState<AvailabilityDay | null>(null); + const [currentAvailabilityTime, setAvailabilityTime] = useState<AvailabilityTime | null>(null); + + 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]); + + useEffect(() => { + console.log(days) + }, [days]) + + function createAvailabilitiesBasedOnUnspecifiedInitialDate(numberOfDays: number, tz: string) { + createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays); + } + + function 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); + } + + function clearAvailabilityTimeSelection() { + setAvailabilityDayBeingSelectedFor(null); + setAvailabilityTime(null); + } + + function beginAvailabilityTimeSelection(e: React.MouseEvent<HTMLDivElement, MouseEvent>, day: AvailabilityDay, startTime: Dayjs) { + setAvailabilityDayBeingSelectedFor(day); + setAvailabilityTime({ + fromTime: startTime, + toTime: startTime + }); + } + + function finishAvailabilityTimeSelection(e: React.MouseEvent<HTMLDivElement, MouseEvent>, day: AvailabilityDay) { + if (currentAvailabilityTime === null) { + return; + } + + day.availableTimes.push(currentAvailabilityTime); + setDays([...days]) + + clearAvailabilityTimeSelection(); + } + + function addTimeToAvailabilityTimeSelection(e: React.MouseEvent<HTMLDivElement, MouseEvent>, day: AvailabilityDay, time: Dayjs) { + if (e.buttons !== 1) { + return; + } + + if (currentAvailabilityTime === null) { + return; + } + + if (currentAvailabilityTime !== null && selectingAvailabilityForDay !== null && Math.abs(selectingAvailabilityForDay.forDate.diff(time, "day")) >= 1) { + clearAvailabilityTimeSelection(); + return; + } + + let currentFrom = currentAvailabilityTime.fromTime; + let currentTo = currentAvailabilityTime.toTime; + + if (time.isBefore(currentFrom)) { + setAvailabilityTime({ + fromTime: time, + toTime: currentTo + }) + + return; + } + + if (time.isAfter(currentTo)) { + setAvailabilityTime({ + fromTime: currentFrom, + toTime: time + }) + + return; + } + } + + function currentAvailabilityTimeSelectionIncludes(time: Dayjs): boolean { + if (currentAvailabilityTime === null) { + return false; + } + + if ((time.isAfter(currentAvailabilityTime.fromTime) && time.isBefore(currentAvailabilityTime.toTime)) || (time.isSame(currentAvailabilityTime.toTime) || time.isSame(currentAvailabilityTime.fromTime))) { + return true; + } + + return false; + } + + function isTimeIncludedInAnyAvailabilityPeriod(day: AvailabilityDay, time: Dayjs): boolean { + return day.availableTimes.some(t => t.fromTime.isBefore(time) && t.toTime.isAfter(time)); + } + + function isTimeBeginningOfAnyAvailabilityPeriod(day: AvailabilityDay, time: Dayjs): boolean { + return day.availableTimes.some(t => t.fromTime.isSame(time)); + } + + function isTimeEndingOfAnyAvailabilityPeriod(day: AvailabilityDay, time: Dayjs): boolean { + return day.availableTimes.some(t => t.toTime.isSame(time)); + } + + function generateDay(day: AvailabilityDay) { + + const HOVER_COLOR: String = "#004455"; + const HOUR_LIGHT_COLOR: String = "#002233"; + const HOUR_DARK_COLOR: String = "#003344"; + const HOUR_BORDER_COLOR: String = "#777"; + const ACTIVE_COLOR: String = "#223300"; + const CURRENTLY_SELECTED_COLOR: String = "#112200"; + const HOUR_TEXT_COLOR: String = "#ddd"; + const HALFHOUR_BORDER_COLOR: String = "#333"; + + let hours = [...Array<String>(24)].map((_, i) => { + let time = day.forDate.set("hour", i).set("minute", 0).set("second", 0); + + return ( + <Box + key={`${i}`} + sx={{ + width: "100%", + borderBottom: 1, + borderColor: HOUR_BORDER_COLOR, + bgcolor: (i % 2 == 0) ? HOUR_LIGHT_COLOR : HOUR_DARK_COLOR, + ":hover": { + bgcolor: HOVER_COLOR + } + }} + > + <Box + sx={{ + width: "100%", + height: HALFHOUR_DISPLAY_HEIGHT, + borderBottom: 1, + borderColor: HALFHOUR_BORDER_COLOR, + ":active": { + bgcolor: ACTIVE_COLOR + }, + bgcolor: currentAvailabilityTimeSelectionIncludes(time) ? CURRENTLY_SELECTED_COLOR : "inherit" + }} + onMouseDown={(e) => beginAvailabilityTimeSelection(e, day, time)} + onMouseUp={(e) => finishAvailabilityTimeSelection(e, day)} + onMouseOver={(e) => addTimeToAvailabilityTimeSelection(e, day, time)} + > + <Typography + className={"noselect"} + textAlign={"left"} + fontSize={"0.65em"} + color={HOUR_TEXT_COLOR} + > + { utils.formatTimeFromHourOfDay(i, 0) } + </Typography> + </Box> + <Box + sx={{ + width: "100%", + height: HALFHOUR_DISPLAY_HEIGHT, + ":active": { + bgcolor: ACTIVE_COLOR + }, + bgcolor: currentAvailabilityTimeSelectionIncludes(time.set("minute", 30)) ? CURRENTLY_SELECTED_COLOR : "inherit" + }} + onMouseDown={(e) => beginAvailabilityTimeSelection(e, day, time.set("minute", 30))} + onMouseUp={(e) => finishAvailabilityTimeSelection(e, day)} + onMouseOver={(e) => addTimeToAvailabilityTimeSelection(e, day, time.set("minute", 30))} + > + </Box> + </Box> + + ); + }) + + return ( + <Stack + key={day.forDate.format()} + direction="column" + sx={{ + minWidth: DAY_DISPLAY_WIDTH, + width: DAY_DISPLAY_WIDTH + }} + overflow={"visible"} + > + <Card + sx={{ + width: "100%", + height: "fit-content", + overflow: "visible" + }} + variant="outlined" + onMouseLeave={(e) => clearAvailabilityTimeSelection()} + > + <Box + sx={{ width: "100%" }} + padding={1} + > + { + (props.eventType === EventTypes.WEEK) && + <Typography> + { day.forDate.format("dddd") } + </Typography> + } + { + (props.eventType === EventTypes.DAY) && + <Typography> + Any Day + </Typography> + } + { + (props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) && + <Typography> + { day.forDate.format("LL") } + </Typography> + } + </Box> + <Divider></Divider> + {hours} + </Card> + </Stack> + ); + } + + return ( + <Stack + direction="row" + spacing={1} + justifyContent={"safe center"} + sx={{ + width: "100%", + height: "auto", + maxHeight: "500px", + overflowY: "scroll", + overflowX: "scroll" + }} + > + { + days.map(a => generateDay(a)) + } + </Stack> + ); +} \ No newline at end of file diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..47e7dff --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,15 @@ +import { Stack, Typography } from '@mui/material'; + +export default function Footer() { + return ( + <Stack + sx={{ height: "50px" }} + direction="column" + justifyContent="center" + > + <Typography align="center"> + Created by <a href="https://mvvasilev.dev">mvvasilev</a> | <a href="https://github.com/mvvasilev/findtheti.me">Github</a> + </Typography> + </Stack> + ); +} \ No newline at end of file diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..135217a --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,30 @@ +import { Stack, Typography, useTheme } from '@mui/material'; + +export default function Header() { + const theme = useTheme(); + + return ( + <Stack + sx={{ height: "100px" }} + direction="column" + justifyContent="center" + > + <a href={window.location.origin}> + <Typography + align="center" + sx={{ + fontFamily: "'Bungee Spice', sans-serif", + [theme.breakpoints.up("xs")]: { + fontSize: "2em" + }, + [theme.breakpoints.up("sm")]: { + fontSize: "4em" + } + }} + > + findtheti.me + </Typography> + </a> + </Stack> + ); +} \ No newline at end of file diff --git a/frontend/src/pages/ExistingEventPage.tsx b/frontend/src/pages/ExistingEventPage.tsx index 89193b0..26310a2 100644 --- a/frontend/src/pages/ExistingEventPage.tsx +++ b/frontend/src/pages/ExistingEventPage.tsx @@ -1,11 +1,82 @@ +import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; +import { Event, 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 utils from "../utils"; export default function ExistingEventPage() { - let { pasteId } = useParams(); + let { eventId } = useParams(); - console.log(pasteId); + const [event, setEvent] = useState<Event>(createEvent()); + + 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 + })); + }, [eventId]); return ( - <div /> + <Grid container sx={{ p: 2 }} spacing={1}> + <Grid xs={12}> + <Typography>You've been invited to...</Typography> + </Grid> + <Grid xs={12}> + <Typography variant="h4">{ event.name }</Typography> + </Grid> + { + (event.description !== null) && + <Grid xs={12}> + <Typography>{ event.description }</Typography> + </Grid> + } + <Grid xs={12}> + <Typography> + This event lasts for { utils.formatMinutesAsHoursMinutes(event.duration) }. When will you be available to attend? + </Typography> + </Grid> + <Grid xs={12}> + { + (event.fromDate !== null && event.toDate !== null && event.eventType !== null) && + <AvailabilityPicker + fromDate={event.fromDate} + toDate={event.toDate} + eventType={event.eventType} + availabilityDurationInMinutes={event.duration} + /> + } + </Grid> + <Grid xs={0} md={3}></Grid> + <Grid xs={12} md={6} container spacing={1}> + <Grid xs={12} sm={9}> + <TextField + sx={{ width: "100%" }} + // TODO + // value={event.description} + // onChange={(e) => { + // event.description = e.target.value; + // setEvent({...event}); + // }} + label="Your Name" + /> + </Grid> + <Grid xs={12} sm={3}> + <Button sx={{ width: "100%", height: "100%" }} variant="contained"> + <Typography>Submit</Typography> + </Button> + </Grid> + </Grid> + <Grid xs={0} md={3}></Grid> + </Grid> ); } \ No newline at end of file diff --git a/frontend/src/pages/NewEventPage.tsx b/frontend/src/pages/NewEventPage.tsx index 1ff88ea..3c4dd6c 100644 --- a/frontend/src/pages/NewEventPage.tsx +++ b/frontend/src/pages/NewEventPage.tsx @@ -1,5 +1,210 @@ +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 { useEffect, useState } from 'react'; +import { useNavigate } from "react-router-dom" +import { Event, EventTypes, createEvent } from '../types/Event'; +import utils from '../utils'; + export default function NewEventPage() { + const navigate = useNavigate(); + + const [event, setEvent] = useState<Event>(createEvent()); + const [isEventValid, setEventValid] = useState<Boolean>(false); + + useEffect(() => { + validateEvent(); + }, [event]) + + function validateEvent(): void { + console.log(event); + var valid: boolean = true; + + valid &&= 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; + } + + if (event.eventType === EventTypes.SPECIFIC_DATE) { + valid &&= event.fromDate !== null; + } + + setEventValid(valid); + } + + function saveEvent() { + fetch("/api/events", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + from_date: event.fromDate?.utc().format(), + to_date: event.toDate?.utc().format(), + name: event.name, + description: event.description, + event_type: event.eventType, + duration: event.duration + }) + }) + .then(resp => resp.json()) + .then(resp => { + navigate(resp.result.snowflake_id) + }) + } + return ( - <div /> + <Grid container> + <Grid xs={12} spacing={1}> + <h2>Create New Event</h2> + </Grid> + <Grid xs={0} sm={2} md={4}></Grid> + <Grid sx={{ p: 2 }} container spacing={1} xs={12} sm={8} md={4}> + <Grid xs={12}> + <TextField + sx={{ width: "100%" }} + value={event.name} + onChange={(e) => { + event.name = e.target.value; + setEvent({...event}); + }} + label="I'm organizing a(n)..." + /> + </Grid> + <Grid xs={12}> + <TextField + sx={{ width: "100%" }} + value={event.description} + onChange={(e) => { + event.description = e.target.value; + setEvent({...event}); + }} + label="More details... ( Optional )" + /> + </Grid> + <Grid xs={12}> + <Typography> + Duration + </Typography> + <Slider + sx={{ width: "90%" }} + step={30} + valueLabelDisplay="auto" + valueLabelFormat={(val) => utils.formatMinutesAsHoursMinutes(val)} + marks={ + [ + { + value: 30, + label: "30m" + }, + { + value: 120, + label: "2h" + }, + { + value: 240, + label: "4h" + }, + { + value: 360, + label: "6h" + }, + { + value: 480, + label: "8h" + } + ] + } + min={30} + max={480} + value={event.duration} + onChange={(_, val) => { + event.duration = val as number; + setEvent({...event}); + }} + /> + </Grid> + <Grid xs={12}> + <Select + sx={{ width: "100%" }} + value={event.eventType} + onChange={(e) => { + event.eventType = e.target.value; + setEvent({...event}); + }} + > + <MenuItem value={EventTypes.UNKNOWN} disabled>Event Type</MenuItem> + <MenuItem value={EventTypes.SPECIFIC_DATE}>Exact Date</MenuItem> + <MenuItem value={EventTypes.DATE_RANGE}>Between</MenuItem> + <MenuItem value={EventTypes.DAY}>Daily</MenuItem> + <MenuItem value={EventTypes.WEEK}>Weekly</MenuItem> + </Select> + </Grid> + { + event.eventType == EventTypes.SPECIFIC_DATE && + <Grid xs={12}> + <DateTimePicker + sx={{ width: "100%" }} + value={event.fromDate} + onChange={(value) => { + event.fromDate = value ?? null; + setEvent({...event}); + }} + label="When" + /> + </Grid> + } + { + event.eventType == EventTypes.DATE_RANGE && + <Grid xs={12} sm={6}> + <DateTimePicker + sx={{ width: "100%" }} + value={event.fromDate} + onChange={(value) => { + event.fromDate = value ?? null; + setEvent({...event}); + }} + label="From" + /> + </Grid> + } + { + event.eventType == EventTypes.DATE_RANGE && + <Grid xs={12} sm={6}> + <DateTimePicker + sx={{ width: "100%" }} + value={event.toDate} + onChange={(value) => { + event.toDate = value ?? null; + setEvent({...event}); + }} + label="To" + /> + </Grid> + } + { + (event.eventType == EventTypes.DAY || event.eventType == EventTypes.WEEK || event.eventType == EventTypes.MONTH) && + <Grid xs={12}> + <Alert severity={"info"}> + <Typography>Selecting the Day type will allow attendees to select their availability during an unspecified {event.eventType}</Typography> + </Alert> + </Grid> + } + <Grid xs={12}> + <Button + disabled={!isEventValid} + sx={{ width: "100%" }} + variant={"contained"} + onClick={saveEvent} + > + <Typography>Create</Typography> + </Button> + </Grid> + </Grid> + <Grid xs={0} sm={2} md={4}></Grid> + </Grid> ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/RootLayout.tsx b/frontend/src/pages/RootLayout.tsx index 708e414..01e6a7c 100644 --- a/frontend/src/pages/RootLayout.tsx +++ b/frontend/src/pages/RootLayout.tsx @@ -1,5 +1,15 @@ import { ThemeProvider } from "@emotion/react"; -import { Box, CssBaseline, createTheme } from "@mui/material"; +import { 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"; +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'; + +dayjs.extend(utc); const theme = createTheme({ palette: { @@ -12,21 +22,51 @@ export default function RootLayout(props: { children: React.ReactNode }) { <ThemeProvider theme={theme}> <CssBaseline/> - <Box + <GithubCorner + href={"https://github.com/mvvasilev/findtheti.me"} + bannerColor="#FD6C6C" + octoColor="inherit" + size={80} + direction="right" + /> + + <Paper component="main" sx={{ position: "absolute", - top: "0", + top: 0, left: "50%", transform: "translate(-50%, 0)", width: { xs: "100%", lg: "1000px" + }, + [theme.breakpoints.up('lg')]: { + top: "25px", + width: "1000px" } }} > - {props.children} - </Box> + <LocalizationProvider dateAdapter={AdapterDayjs}> + <Grid container> + <Grid xs={12}> + <Header /> + </Grid> + <Grid xs={12}> + <Divider></Divider> + </Grid> + <Grid xs={12}> + {props.children} + </Grid> + <Grid xs={12}> + <Divider></Divider> + </Grid> + <Grid xs={12}> + <Footer /> + </Grid> + </Grid> + </LocalizationProvider> + </Paper> </ThemeProvider> ); }; \ No newline at end of file diff --git a/frontend/src/types/Event.tsx b/frontend/src/types/Event.tsx new file mode 100644 index 0000000..b04609e --- /dev/null +++ b/frontend/src/types/Event.tsx @@ -0,0 +1,32 @@ +import { Dayjs } from "dayjs"; + +export type Event = { + snowflakeId: String, + name: String, + description: String, + fromDate: null | Dayjs, + toDate: null | Dayjs, + eventType: String, + duration: number +}; + +export const EventTypes = { + UNKNOWN: "Unknown", + SPECIFIC_DATE: "SpecificDate", + DATE_RANGE: "DateRange", + DAY: "Day", + WEEK: "Week", + MONTH: "Month" // Unsupported atm +}; + +export function createEvent(): Event { + return { + snowflakeId: "", + name: "", + description: "", + fromDate: null, + toDate: null, + eventType: EventTypes.UNKNOWN, + duration: 30 + }; +} \ No newline at end of file diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts new file mode 100644 index 0000000..e10c8c9 --- /dev/null +++ b/frontend/src/utils.ts @@ -0,0 +1,30 @@ +import dayjs from "dayjs"; +import * as duration from 'dayjs/plugin/duration'; + +dayjs.extend(duration) + +const utils = { + toHoursAndMinutes: (totalMinutes: number): { hours: number, minutes: number } => { + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + return { hours, minutes }; + }, + formatMinutesAsHoursMinutes: (val: number): String => { + let { hours, minutes } = utils.toHoursAndMinutes(val); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else { + return `${minutes}m`; + } + }, + zeroPad: (num: number, places: number): String => { + return String(num).padStart(places, '0'); + }, + formatTimeFromHourOfDay: (hourOfDay: number, minutes: number): String => { + return dayjs.duration({ hours: hourOfDay, minutes: minutes }).format('HH:mm'); + } +} + +export default utils; \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 54e3a72..21d76fe 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,6 +6,15 @@ import { fileURLToPath, URL } from 'node:url' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server: { + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + }, + } + }, resolve: { alias: [ { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 34c44de..673a3f3 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -28,7 +28,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.23.5": +"@babel/core@^7.23.5": version "7.23.7" resolved "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz" integrity sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw== @@ -186,6 +186,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.23.2": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz" @@ -265,7 +272,7 @@ resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== -"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.11.3", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0": +"@emotion/react@^11.11.3": version "11.11.3" resolved "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz" integrity sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA== @@ -295,7 +302,7 @@ resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz" integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== -"@emotion/styled@^11.11.0", "@emotion/styled@^11.3.0": +"@emotion/styled@^11.11.0": version "11.11.0" resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz" integrity sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng== @@ -327,11 +334,121 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz" integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== +"@esbuild/aix-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" + integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g== + +"@esbuild/android-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220" + integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q== + +"@esbuild/android-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c" + integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw== + +"@esbuild/android-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2" + integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg== + +"@esbuild/darwin-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf" + integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ== + +"@esbuild/darwin-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e" + integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g== + +"@esbuild/freebsd-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a" + integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA== + +"@esbuild/freebsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2" + integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw== + +"@esbuild/linux-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545" + integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg== + +"@esbuild/linux-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3" + integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q== + +"@esbuild/linux-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4" + integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA== + +"@esbuild/linux-loong64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121" + integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg== + +"@esbuild/linux-mips64el@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9" + integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg== + +"@esbuild/linux-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912" + integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA== + +"@esbuild/linux-riscv64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916" + integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ== + +"@esbuild/linux-s390x@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8" + integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q== + "@esbuild/linux-x64@0.19.11": version "0.19.11" resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz" integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA== +"@esbuild/netbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d" + integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ== + +"@esbuild/openbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2" + integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw== + +"@esbuild/sunos-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767" + integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ== + +"@esbuild/win32-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee" + integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ== + +"@esbuild/win32-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c" + integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg== + +"@esbuild/win32-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04" + integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" @@ -442,7 +559,7 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@mui/base@5.0.0-beta.30": +"@mui/base@5.0.0-beta.30", "@mui/base@^5.0.0-beta.22": version "5.0.0-beta.30" resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.30.tgz" integrity sha512-dc38W4W3K42atE9nSaOeoJ7/x9wGIfawdwC/UmMxMLlZ1iSsITQ8dQJaTATCbn98YvYPINK/EH541YA5enQIPQ== @@ -516,7 +633,7 @@ resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.12.tgz" integrity sha512-3kaHiNm9khCAo0pVe0RenketDSFoZGAlVZ4zDjB/QNZV0XiCj+sh1zkX0VVhQPgYJDlBEzAag+MHJ1tU3vf0Zw== -"@mui/utils@^5.15.3": +"@mui/utils@^5.14.16", "@mui/utils@^5.15.3": version "5.15.3" resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.15.3.tgz" integrity sha512-mT3LiSt9tZWCdx1pl7q4Q5tNo6gdZbvJel286ZHGuj6LQQXjWNAh8qiF9d+LogvNUI+D7eLkTnj605d1zoazfg== @@ -526,6 +643,19 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/x-date-pickers@^6.18.7": + version "6.18.7" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.18.7.tgz#6b00163c77dc450c11b44a479baf62541e6f8b36" + integrity sha512-4NoapaCT3jvEk2cuAUjG0ReZvTEk1i4dGDz94Gt1Oc08GuC1AuzYRwCR1/1tdmbDynwkR8ilkKL6AyS3NL1H4A== + dependencies: + "@babel/runtime" "^7.23.2" + "@mui/base" "^5.0.0-beta.22" + "@mui/utils" "^5.14.16" + "@types/react-transition-group" "^4.4.8" + clsx "^2.0.0" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -534,7 +664,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -557,6 +687,46 @@ resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.14.1.tgz" integrity sha512-Qg4DMQsfPNAs88rb2xkdk03N3bjK4jgX5fR24eHCTR9q6PrhZQZ4UJBPzCHJkIpTRN1UKxx2DzjZmnC+7Lj0Ow== +"@rollup/rollup-android-arm-eabi@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz#b1094962742c1a0349587040bc06185e2a667c9b" + integrity sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA== + +"@rollup/rollup-android-arm64@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz#96eb86fb549e05b187f2ad06f51d191a23cb385a" + integrity sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA== + +"@rollup/rollup-darwin-arm64@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.4.tgz#2456630c007cc5905cb368acb9ff9fc04b2d37be" + integrity sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA== + +"@rollup/rollup-darwin-x64@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.4.tgz#97742214fc7dfd47a0f74efba6f5ae264e29c70c" + integrity sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA== + +"@rollup/rollup-linux-arm-gnueabihf@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.4.tgz#cd933e61d6f689c9cdefde424beafbd92cfe58e2" + integrity sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw== + +"@rollup/rollup-linux-arm64-gnu@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.4.tgz#33b09bf462f1837afc1e02a1b352af6b510c78a6" + integrity sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg== + +"@rollup/rollup-linux-arm64-musl@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.4.tgz#50257fb248832c2308064e3764a16273b6ee4615" + integrity sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A== + +"@rollup/rollup-linux-riscv64-gnu@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.4.tgz#09589e4e1a073cf56f6249b77eb6c9a8e9b613a8" + integrity sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A== + "@rollup/rollup-linux-x64-gnu@4.9.4": version "4.9.4" resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.4.tgz" @@ -567,6 +737,21 @@ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.4.tgz" integrity sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ== +"@rollup/rollup-win32-arm64-msvc@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.4.tgz#95957067eb107f571da1d81939f017d37b4958d3" + integrity sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ== + +"@rollup/rollup-win32-ia32-msvc@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.4.tgz#71b6facad976db527863f698692c6964c0b6e10e" + integrity sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ== + +"@rollup/rollup-win32-x64-msvc@4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.4.tgz#16295ccae354707c9bc6842906bdeaad4f3ba7a5" + integrity sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw== + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" @@ -610,7 +795,7 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@^18.0.0 || >=20.0.0", "@types/node@^20.10.7": +"@types/node@^20.10.7": version "20.10.7" resolved "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz" integrity sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg== @@ -634,14 +819,14 @@ dependencies: "@types/react" "*" -"@types/react-transition-group@^4.4.10": +"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.8": version "4.4.10" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz" integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^17.0.0 || ^18.0.0", "@types/react@^18.2.43": +"@types/react@*", "@types/react@^18.2.43": version "18.2.47" resolved "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz" integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ== @@ -677,7 +862,7 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.0.0 || ^6.0.0-alpha", "@typescript-eslint/parser@^6.14.0": +"@typescript-eslint/parser@^6.14.0": version "6.18.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.0.tgz" integrity sha512-v6uR68SFvqhNQT41frCMCQpsP+5vySy6IdgjlzUWoo7ALCnpaWYcz/Ij2k4L8cEsL0wkvOviCMpjmtRtHNOKzA== @@ -767,7 +952,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: +acorn@^8.9.0: version "8.11.3" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -847,7 +1032,7 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.22.2, "browserslist@>= 4.21.0": +browserslist@^4.22.2: version "4.22.2" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz" integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== @@ -903,16 +1088,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -953,6 +1138,11 @@ csstype@^3.0.2, csstype@^3.1.2: resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +dayjs@^1.11.10: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -1066,7 +1256,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0", eslint@^8.55.0, eslint@>=7: +eslint@^8.55.0: version "8.56.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz" integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== @@ -1222,6 +1412,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -1489,13 +1684,6 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - minimatch@9.0.3: version "9.0.3" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" @@ -1503,6 +1691,13 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" @@ -1646,7 +1841,7 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -"react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0: +react-dom@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -1654,6 +1849,11 @@ queue-microtask@^1.2.2: loose-envify "^1.1.0" scheduler "^0.23.0" +react-github-corner@^2.5.0: + version "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-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" @@ -1694,7 +1894,7 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -"react@^17.0.0 || ^18.0.0", react@^18.2.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0: +react@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -1877,7 +2077,7 @@ type-fest@^0.20.2: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.2.2, typescript@>=4.2.0: +typescript@^5.2.2: version "5.3.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== @@ -1902,7 +2102,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -"vite@^4.2.0 || ^5.0.0", vite@^5.0.8: +vite@^5.0.8: version "5.0.11" resolved "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz" integrity sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA== diff --git a/migrations/20240108212119_EventSetDatesOptional.sql b/migrations/20240108212119_EventSetDatesOptional.sql new file mode 100644 index 0000000..82c27dd --- /dev/null +++ b/migrations/20240108212119_EventSetDatesOptional.sql @@ -0,0 +1,11 @@ +ALTER TABLE events.event +DROP COLUMN IF EXISTS from_date; + +ALTER TABLE events.event +DROP COLUMN IF EXISTS to_date; + +ALTER TABLE events.event +ADD COLUMN IF NOT EXISTS from_date TIMESTAMP NULL; + +ALTER TABLE events.event +ADD COLUMN IF NOT EXISTS to_date TIMESTAMP NULL; \ No newline at end of file diff --git a/migrations/20240109082813_EventAddDuration.sql b/migrations/20240109082813_EventAddDuration.sql new file mode 100644 index 0000000..1ece1a6 --- /dev/null +++ b/migrations/20240109082813_EventAddDuration.sql @@ -0,0 +1,2 @@ +ALTER TABLE events.event +ADD COLUMN IF NOT EXISTS duration INTEGER NOT NULL DEFAULT (60); \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index 402adfb..611ab7f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -40,8 +40,8 @@ pub(crate) async fn insert_event_and_fetch_id( ) -> Result<i64, sqlx::Error> { sqlx::query_scalar!( r#" - INSERT INTO events.event (snowflake_id, name, description, from_date, to_date, event_type) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO events.event (snowflake_id, name, description, from_date, to_date, event_type, duration) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id "#, event.snowflake_id, @@ -49,7 +49,8 @@ pub(crate) async fn insert_event_and_fetch_id( event.description, event.from_date, event.to_date, - event.event_type.to_string() + event.event_type.to_string(), + event.duration ) .fetch_one(&mut **txn) .await diff --git a/src/endpoints.rs b/src/endpoints.rs index ba6de67..320f9d2 100644 --- a/src/endpoints.rs +++ b/src/endpoints.rs @@ -18,21 +18,23 @@ use crate::{ #[derive(Deserialize)] pub struct CreateEventDto { - from_date: DateTime<Utc>, - to_date: DateTime<Utc>, + from_date: Option<DateTime<Utc>>, + to_date: Option<DateTime<Utc>>, name: String, description: Option<String>, event_type: String, + duration: i32 } #[derive(Serialize)] pub struct EventDto { snowflake_id: String, - from_date: DateTime<Utc>, - to_date: DateTime<Utc>, + from_date: Option<DateTime<Utc>>, + to_date: Option<DateTime<Utc>>, name: String, description: Option<String>, event_type: String, + duration: i32 } #[derive(Deserialize)] @@ -92,9 +94,10 @@ pub async fn create_event( snowflake_id: uid, name: dto.name, description: dto.description, - from_date: dto.from_date.naive_utc(), - to_date: dto.to_date.naive_utc(), + from_date: dto.from_date.map(|d| d.naive_utc()), + to_date: dto.to_date.map(|d| d.naive_utc()), event_type, + duration: dto.duration }, ) .await?; @@ -103,11 +106,12 @@ pub async fn create_event( Ok(EventDto { snowflake_id: event.snowflake_id, - from_date: Utc.from_utc_datetime(&event.from_date), - to_date: Utc.from_utc_datetime(&event.to_date), + from_date: event.from_date.map(|d| Utc.from_utc_datetime(&d)), + to_date: event.to_date.map(|d| Utc.from_utc_datetime(&d)), name: event.name, description: event.description, event_type: event.event_type.to_string(), + duration: event.duration }) }) }) @@ -132,11 +136,12 @@ pub async fn fetch_event( Ok(EventDto { snowflake_id: event.snowflake_id, - from_date: Utc.from_utc_datetime(&event.from_date), - to_date: Utc.from_utc_datetime(&event.to_date), + from_date: event.from_date.map(|d| Utc.from_utc_datetime(&d)), + to_date: event.to_date.map(|d| Utc.from_utc_datetime(&d)), name: event.name, description: event.description, event_type: event.event_type.to_string(), + duration: event.duration }) }) }) diff --git a/src/entity/event.rs b/src/entity/event.rs index 08e2a29..3fc4fec 100644 --- a/src/entity/event.rs +++ b/src/entity/event.rs @@ -7,9 +7,10 @@ pub(crate) struct Event { pub snowflake_id: String, pub name: String, pub description: Option<String>, - pub from_date: NaiveDateTime, - pub to_date: NaiveDateTime, + pub from_date: Option<NaiveDateTime>, + pub to_date: Option<NaiveDateTime>, pub event_type: EventType, + pub duration: i32 } #[derive(Debug)] diff --git a/src/main.rs b/src/main.rs index bd0c053..4151920 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,10 +16,15 @@ async fn main() { let api_routes = api::routes().await.expect("Unable to create api routes"); - let routes = Router::new() - .nest("/api", api_routes) - .nest_service("/", ServeDir::new("./frontend/dist")) - .fallback_service(ServeDir::new("./frontend/dist")); + let mut routes = Router::new() + .nest("/api", api_routes); + + + // If in release mod, serve static files + if !cfg!(debug_assertions) { + routes = routes.nest_service("/", ServeDir::new("./frontend/dist")) + .fallback_service(ServeDir::new("./frontend/dist")); + } let addr = SocketAddr::from(([127, 0, 0, 1], 8080));