diff --git a/src/pages/index.tsx b/src/pages/index.tsx index bdaae65688f02ab1e75b6a02f916a4c851efffa5..d788f8f0bd0e12458499ed4b084017f0c7bee6ff 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,6 +11,7 @@ import { LectureLiveLabel } from "@/videoag/course/LiveLabel"; import { ErrorComponent, showError, showWarningToast } from "@/videoag/error/ErrorDisplay"; import { FallbackErrorBoundary } from "@/videoag/error/FallbackErrorBoundary"; import { useLanguage } from "@/videoag/localization/LanguageProvider"; +import { datetimeToStringOnlyDate, datetimeToStringOnlyTime } from "@/videoag/miscellaneous/Formatting"; import { ReloadBoundary, useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary"; import { ResourceType, @@ -170,13 +171,7 @@ function UpcomingUploads({ homepageData }: { homepageData: ResourceType<GetHomep let dates = []; // group by day/date for (let lecture of data.upcoming_lectures) { - let date_parsed = new Date(lecture.time); - let date_string = date_parsed.toLocaleDateString([], { - weekday: "short", - year: "numeric", - month: "2-digit", - day: "2-digit", - }); + let date_string = datetimeToStringOnlyDate(lecture.time); let date_index = dates.findIndex((date) => date.date === date_string); if (date_index === -1) { @@ -193,7 +188,6 @@ function UpcomingUploads({ homepageData }: { homepageData: ResourceType<GetHomep <strong>{date.date}</strong> <ul className="list-group"> {date.lectures.map((lecture, index) => { - let date_parsed = new Date(lecture.time); let course_data = data.course_context[lecture.course_id]; return ( <li @@ -201,13 +195,7 @@ function UpcomingUploads({ homepageData }: { homepageData: ResourceType<GetHomep style={{ border: "none" }} key={index} > - { - // only time (HH:MM) - date_parsed.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }) - }{" "} + {datetimeToStringOnlyTime(lecture.time)}{" "} <a href={`/${course_data.handle}`}>{course_data.full_name}</a> {": "} <a href={`/${course_data.handle}#lecture-${lecture.id}`}> diff --git a/src/videoag/course/Lecture.tsx b/src/videoag/course/Lecture.tsx index 52b2d14985a35552de6003c3a1fc073f4fa2db10..fe9e5ab5fac48c2020c6f98be309160dbdd21a39 100644 --- a/src/videoag/course/Lecture.tsx +++ b/src/videoag/course/Lecture.tsx @@ -263,8 +263,6 @@ export function LectureCard({ size?: "small" | "auto"; }) { const { language } = useLanguage(); - let dateStr = datetimeToString(lecture.time); - let sz = size === "small" ? "xxl" : "sm"; return ( @@ -306,7 +304,7 @@ export function LectureCard({ </span>{" "} <br /> <br /> - <span>{dateStr}</span> + <span>{datetimeToString(lecture.time)}</span> {lecture.speaker ? ( <div className="small p-children-inline"> {language.get("ui.generic.lecture_given_by")}{" "} @@ -345,7 +343,7 @@ export function LectureCard({ <strong>{course.full_name}</strong> <LectureLiveLabel lecture={lecture} /> </li> - <li>{dateStr}</li> + <li>{datetimeToString(lecture.time)}</li> {lecture.speaker ? ( <div className="small p-children-inline"> {language.get("ui.generic.lecture_given_by")}{" "} diff --git a/src/videoag/course/Player.tsx b/src/videoag/course/Player.tsx index 2a296dc7eaa99eb54aa4d66a02dc9fb01124a95f..70dbc311d101476e1ef01f4adc9cfde10d372876 100644 --- a/src/videoag/course/Player.tsx +++ b/src/videoag/course/Player.tsx @@ -16,7 +16,7 @@ import { useLanguage } from "@/videoag/localization/LanguageProvider"; import Title from "@/videoag/miscellaneous/TitleComponent"; import { ResourceType } from "@/videoag/miscellaneous/PromiseHelpers"; import { UpdateOverlay } from "@/videoag/miscellaneous/UpdateOverlay"; -import { showInfoToast } from "@/videoag/miscellaneous/Util"; +import { showInfoToast, parseApiDatetime } from "@/videoag/miscellaneous/Util"; import { useEditMode } from "@/videoag/object_management/EditModeProvider"; import { OMDelete, @@ -198,12 +198,12 @@ function LectureSuggestions({ course, lecture }: { course: course; lecture: lect const { language } = useLanguage(); const prevLecture = course.lectures?.findLast( (l) => - new Date(l.time) < new Date(lecture.time) && + parseApiDatetime(l.time) < parseApiDatetime(lecture.time) && (l.publish_media ?? []).find((m) => m.include_in_player), ); const nextLecture = course.lectures?.find( (l) => - new Date(l.time) > new Date(lecture.time) && + parseApiDatetime(l.time) > parseApiDatetime(lecture.time) && (l.publish_media ?? []).find((m) => m.include_in_player), ); diff --git a/src/videoag/form/TypeEditor.tsx b/src/videoag/form/TypeEditor.tsx index 40ab15cb1a07983ac2b4bd4894b6b8f6b5d2531b..ed4e9139868fd59c814d9d683bb2f28c6809d1e4 100644 --- a/src/videoag/form/TypeEditor.tsx +++ b/src/videoag/form/TypeEditor.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import type { int } from "@/videoag/api/types"; import { useLanguage } from "@/videoag/localization/LanguageProvider"; -import { deepEquals } from "@/videoag/miscellaneous/Util"; +import { deepEquals, parseApiDatetime, formatApiDatetime } from "@/videoag/miscellaneous/Util"; export type EditorArgs = { value?: any; @@ -232,7 +232,11 @@ export function DatetimeEditor({ }, [knownValue, value, setForceInputUpdate]); const isValidCheck = (val: string | null | undefined) => { - if (val === null || val === undefined || isNaN(new Date(val).valueOf())) { + let parsedVal = null; + try { + parsedVal = parseApiDatetime(val); + } catch {} + if (val === null || val === undefined || parsedVal === null) { return mayBeNull === true; } return true; @@ -246,17 +250,7 @@ export function DatetimeEditor({ isValid = false; } } else { - const date = new Date(newValue); - const intToStr = (v: int, length: int) => { - let str = v.toString(); - while (str.length < length) { - str = "0" + str; - } - return str; - }; - const dateStr = `${intToStr(date.getFullYear(), 4)}-${intToStr(date.getMonth() + 1, 2)}-${intToStr(date.getDate(), 2)}`; - const timeStr = `${intToStr(date.getHours(), 2)}:${intToStr(date.getMinutes(), 2)}:${intToStr(date.getSeconds(), 2)}`; - newValue = `${dateStr}T${timeStr}`; + newValue = formatApiDatetime(new Date(newValue)); } knownValue.current = newValue; updateValue(newValue, isValid, false); diff --git a/src/videoag/miscellaneous/Formatting.tsx b/src/videoag/miscellaneous/Formatting.tsx index 065773367c4b8906a91e90a5eff94fd253e7a8dc..c877dc220b18e8cb1ac70709271ae187a085a3ae 100644 --- a/src/videoag/miscellaneous/Formatting.tsx +++ b/src/videoag/miscellaneous/Formatting.tsx @@ -1,3 +1,5 @@ +import { zeropad, parseApiDatetime } from "./Util"; + export function semesterToHuman(semester?: string, long_str?: boolean): string { if (!semester || semester === "zeitlos" || semester === "none" || semester.length !== 6) { return "Zeitlos"; @@ -16,10 +18,6 @@ export function semesterToHuman(semester?: string, long_str?: boolean): string { return `Wintersemester ${year}/${(parseInt(year) + 1).toString().substring(2)}`; } } -export function zeropad(num: number, places: number) { - var zero = places - num.toString().length + 1; - return Array(+(zero > 0 && zero)).join("0") + num; -} export function timestampToString(timestamp: number) { let h = zeropad(Math.trunc(timestamp / 3600), 2); let m = zeropad(Math.trunc((timestamp % 3600) / 60), 2); @@ -27,10 +25,25 @@ export function timestampToString(timestamp: number) { let timeasstring = h + ":" + m + ":" + s; return timeasstring; } -export function datetimeToString(datetime: string) { - let date_parsed = new Date(datetime); - return `${date_parsed.toLocaleDateString([], { weekday: "short", year: "numeric", month: "2-digit", day: "2-digit" })}, ${date_parsed.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`; + +export function datetimeToStringOnlyDate(datetime: DateTime | string): string { + if (typeof datetime === "string") { + datetime = parseApiDatetime(datetime); + } + return datetime.toLocaleString({ weekday: "short", year: "numeric", month: "2-digit", day: "2-digit" }); +} + +export function datetimeToStringOnlyTime(datetime: DateTime): string { + if (typeof datetime === "string") { + datetime = parseApiDatetime(datetime); + } + return datetime.toLocaleString({ hour: "2-digit", minute: "2-digit" }); +} + +export function datetimeToString(datetime: DateTime): string { + return `${datetimeToStringOnlyDate(datetime)}, ${datetimeToStringOnlyTime(datetime)}`; } + export function filesizeToHuman(size: number) { if (size < 1024) { return size + " B"; diff --git a/src/videoag/miscellaneous/Util.tsx b/src/videoag/miscellaneous/Util.tsx index 12c019c324a7f1d8912f3f909274b52300491d59..8aad538a2491f352e571f7f7059567cd4a108477 100644 --- a/src/videoag/miscellaneous/Util.tsx +++ b/src/videoag/miscellaneous/Util.tsx @@ -1,6 +1,22 @@ import type React from "react"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { toast, Bounce } from "react-toastify"; +import { DateTime } from "luxon"; + +import type { datetime } from "@/videoag/api/types"; + +export function zeropad(num: number, places: number) { + var zero = places - num.toString().length + 1; + return Array(+(zero > 0 && zero)).join("0") + num; +} + +export function formatApiDatetime(datetime: DateTime): string { + return datetime.toUTC().toISO({ format: "extended", suppressSeconds: false, suppressMilliseconds: false, includeOffset: true }); +} + +export function parseApiDatetime(datetimeString: datetime): DateTime { + return DateTime.fromISO(datetimeString); +} export function deepEquals(value: any, other: any) { if (value === null || other === null) {