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));