New Date entry on the new Calendar page is now based off the previous one (#1070)

* Issue 858: On the new calendar page adding a new Date entry with the "Add" button will now insert a new date with an offset of +24 hours of the previous row's date value.

* Added comment to ignore the TSLint "object can be null" error

* Changed `var` to `const`

* Fixed Prettier checks in CI pipeline

* Fixed Typecheck CI pipeline error

* Moved getDateWithHoursOffset() function to app/utils/dates.ts

* Added new line at end of file

* Added getValidNewDateIfInvalid() function. This retrieves a valid date. If invalid, get a new Date object.
- So now, if we intentionally/accidentally delete the data in the Calendar's DateInput element, it will be reset to the current Date/Time

* Refactored DateInput component's update state mechanism to be handled by an onChange() function defined in the parent component that is passed to the child

* Prettier formatting so that the new CI pipeline won't output errors at me

* Removed unused imported types

* Removed the datesCount React Hook & refactored accordingly

* Removed unused loader-related variables

* DateInput onChange prop is now optional

* Instead of generating a new Array, iterate over DateInput's inputState's array instead

* Fix potential undefined error

* DatesInputState: refactored to remove index & access the index during iteration with map() 2nd arg

* Properly initialized state for pre-existing events =)
- Also added TODO comments for improving date input handling (1082)

* Prettier formatting

* Uncommented console.warn()

* Touched up comment
This commit is contained in:
William Lam
2022-10-30 22:59:51 -04:00
committed by GitHub
parent ad61df057c
commit 09851faf83
3 changed files with 106 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
import { useIsMounted } from "~/hooks/useIsMounted";
import { dateToYearMonthDayHourMinuteString } from "~/utils/dates";
import { dateToYearMonthDayHourMinuteString, isValidDate } from "~/utils/dates";
import * as React from "react";
export function DateInput({
@@ -9,6 +9,7 @@ export function DateInput({
min,
max,
required,
onChange,
}: {
id?: string;
name?: string;
@@ -16,6 +17,7 @@ export function DateInput({
min?: Date;
max?: Date;
required?: boolean;
onChange?: (newDate: Date) => void;
}) {
const [date, setDate] = React.useState(defaultValue ?? new Date());
const isMounted = useIsMounted();
@@ -41,7 +43,20 @@ export function DateInput({
value={dateToYearMonthDayHourMinuteString(date)}
min={min ? dateToYearMonthDayHourMinuteString(min) : undefined}
max={max ? dateToYearMonthDayHourMinuteString(max) : undefined}
onChange={(e) => setDate(new Date(e.target.value))}
onChange={(e) => {
//TODO: fix invalid Date Input handling: https://github.com/Sendouc/sendou.ink/issues/1082
const updatedDate = new Date(e.target.value);
if (!isValidDate(updatedDate)) {
console.warn("Invalid date");
// throw new RangeError("Invalid Date");
}
setDate(updatedDate);
// Update the correct entry in the React hook from the parent via the passed on callback function
if (typeof onChange !== "undefined") {
onChange(updatedDate);
}
}}
required={required}
/>
</>

View File

@@ -31,8 +31,9 @@ import { canEditCalendarEvent } from "~/permissions";
import calendarNewStyles from "~/styles/calendar-new.css";
import mapsStyles from "~/styles/maps.css";
import {
databaseTimestampToDate,
dateToDatabaseTimestamp,
databaseTimestampToDate,
getDateWithHoursOffset,
} from "~/utils/dates";
import {
badRequestIfFalsy,
@@ -288,14 +289,31 @@ function DescriptionTextarea() {
function DatesInput() {
const { t } = useTranslation(["common", "calendar"]);
const { eventToEdit } = useLoaderData<typeof loader>();
const [datesCount, setDatesCount] = React.useState(
eventToEdit?.startTimes.length ?? 1
);
const isMounted = useIsMounted();
// Initialize datesInputState by retrieving pre-existing events if they exist
let eventDatesInputState = null;
if (typeof eventToEdit?.startTimes !== "undefined") {
eventDatesInputState = eventToEdit.startTimes.map((t) => {
return { finalDateInputDate: databaseTimestampToDate(t) };
});
}
// React hook that keeps contains an array of parameters that corresponds to each DateInput child object generated
const [datesInputState, setDatesInputState] = React.useState(
eventDatesInputState ?? [
{
finalDateInputDate: new Date(),
},
]
);
const datesCount = datesInputState.length;
const isMounted = useIsMounted();
const usersTimeZone = isMounted
? Intl.DateTimeFormat().resolvedOptions().timeZone
: "";
const NEW_CALENDAR_EVENT_HOURS_OFFSET = 24;
return (
<div className="stack md items-start">
@@ -304,38 +322,58 @@ function DatesInput() {
{t("calendar:forms.dates")}
</Label>
<div className="stack sm">
{new Array(datesCount).fill(null).map((_, i) => {
const defaultStartTime = eventToEdit?.startTimes[i];
{datesInputState.map((inputState, i) => {
return (
<div key={i} className="stack horizontal sm items-center">
<DateInput
id="date"
name="date"
defaultValue={
defaultStartTime
? databaseTimestampToDate(defaultStartTime)
: undefined
}
defaultValue={inputState.finalDateInputDate ?? new Date()}
min={MIN_DATE}
max={MAX_DATE}
required
onChange={(newDate: Date) => {
setDatesInputState((current) =>
current.map((obj, objIndex) => {
if (objIndex === i) {
return { ...obj, finalDateInputDate: newDate };
}
return obj;
})
);
}}
/>
{i === datesCount - 1 && (
<>
{/* "Add" button */}
<Button
tiny
disabled={
datesCount === CALENDAR_EVENT.MAX_AMOUNT_OF_DATES
}
onClick={() => setDatesCount((count) => count + 1)}
onClick={() => {
setDatesInputState((current) => [
...current,
{
finalDateInputDate: getDateWithHoursOffset(
inputState.finalDateInputDate,
NEW_CALENDAR_EVENT_HOURS_OFFSET
),
},
]);
}}
>
{t("common:actions.add")}
</Button>
{/* "Remove" button */}
{datesCount > 1 && (
<Button
tiny
onClick={() => setDatesCount((count) => count - 1)}
onClick={() => {
setDatesInputState((current) => current.slice(0, -1));
}}
variant="destructive"
>
{t("common:actions.remove")}

View File

@@ -34,13 +34,32 @@ export function weekNumberToDate({
return result;
}
/**
* Checks if a date is valid or not.
*
* Returns:
* - True if date is valid
* - False otherwise
*/
export function isValidDate(date: Date) {
return !isNaN(date.getTime());
}
/** Returns date as a string with the format YYYY-MM-DDThh:mm in user's time zone */
export function dateToYearMonthDayHourMinuteString(date: Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const copiedDate = new Date(date.getTime());
//TODO: fix invalid Date Input handling: https://github.com/Sendouc/sendou.ink/issues/1082
if (!isValidDate(copiedDate)) {
console.warn("Invalid date");
// throw new RangeError("Invalid Date");
}
const year = copiedDate.getFullYear();
const month = copiedDate.getMonth() + 1;
const day = copiedDate.getDate();
const hour = copiedDate.getHours();
const minute = copiedDate.getMinutes();
return `${year}-${prefixZero(month)}-${prefixZero(day)}T${prefixZero(
hour
@@ -50,3 +69,15 @@ export function dateToYearMonthDayHourMinuteString(date: Date) {
function prefixZero(number: number) {
return number < 10 ? `0${number}` : number;
}
/**
* Retrieves a new Date object that is offset by several hours.
*
* NOTE: it is important that we work with & return a copy of the date here,
* otherwise we will just be mutating the original date passed into this function.
*/
export function getDateWithHoursOffset(date: Date, hoursOffset: number) {
const copiedDate = new Date(date.getTime());
copiedDate.setHours(date.getHours() + hoursOffset);
return copiedDate;
}