From bb78a3d53d14cef373e5c68bf758da6411b898dd Mon Sep 17 00:00:00 2001 From: Shahan Nedadahandeh Date: Tue, 22 Feb 2022 23:29:53 -0500 Subject: [PATCH] Warning Header (Closes #205) (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update: In order to solve the issues discussed below, we decided to put the warnings in a json file, since they can be easily imported into a javascript file and webpack automatically bundles them with the client side app, so no static props is needed. __________ To get the warning data, since it is something that needs to be on all pages, it makes sense for the component to go in the app.js file (similar to the nav bar). However, next.js has a cumbersome issue that it does not support getStaticProps in the app.ts file (and getStaticProps only works in page files), thus we have no way of requesting the warning data easily in the \ Component. (https://github.com/vercel/next.js/discussions/10949) Here is my solution: - Request the warning data on the client side through the use of an api. - I made a warning api (/api/currentWarning) which sends a json of the current warning - Advantage: warnings will always be up to date since it is recalculated on every request - Advantage: It can potentially incorporate with other CSC services who might need to know about the warning (eg linktree?) - Disadvantage: can get expensive if we have a lot of warnings, to fix this I can cache the current warning and only re-read the files every 24h if this is a problem, but if we don’t have that many warnings I think it should be fine - Disadvantage: listed below: The current problem with what I implemented is that it doesn’t build in the production environment: - The way we build the website, when we call “next export” that disables any api endpoints. - According to this https://github.com/vercel/vercel/discussions/6551, if we want to allow api endpoints, we have to just do “next build”. This has implications that the site won’t be completely static anymore, but the other solutions also have similar problems ( though the GitHub post says that next will still optimize for static with only next build). Another advantage of allowing api’s is that maybe in the future we will implement other features where an api might be useful. If you want to try it out, my branch works fine locally (when just running npm run dev or npm run build”) Other possible solutions: - Use getInitialProps inside _app.js, but this has the downside that it will “disable Automatic Static Optimization in pages without Static Generation.” (https://nextjs.org/docs/advanced-features/custom-app), which I believe will slow the whole site down, but, we will be able to still use the old build command I think, I am not sure of the full effect of this. - However, implementing this would also be relatively simple. - Add the warning request to the getStaticProps of every single page, possibly through some wrapper component around every single page, this has the disadvantage that we need to do a lot of refactoring of all the pages of the site and cant use the intended “_app.ts” wrapper. - Add warning header only to the homepage (or maybe one or two other important pages). Co-authored-by: shahanneda Reviewed-on: https://git.csclub.uwaterloo.ca/www/www-new/pulls/394 Reviewed-by: Amy --- components/Theme.tsx | 2 + components/WarningHeader.module.css | 12 ++++++ components/WarningHeader.tsx | 60 +++++++++++++++++++++++++++++ content/warnings/warnings.json | 12 ++++++ lib/events.ts | 20 ++++------ lib/news.ts | 2 +- pages/_app.css | 2 + pages/_app.tsx | 2 + scripts/change-dates.ts | 3 +- utils.ts | 12 ++++++ 10 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 components/WarningHeader.module.css create mode 100644 components/WarningHeader.tsx create mode 100644 content/warnings/warnings.json diff --git a/components/Theme.tsx b/components/Theme.tsx index 78729d3c..f9d8b4bc 100644 --- a/components/Theme.tsx +++ b/components/Theme.tsx @@ -34,6 +34,8 @@ export const PALETTE_NAMES = [ "--text", "--form-invalid", + "--warning-background", + "--warning-text", "--input-background", "--input-placeholder-text", diff --git a/components/WarningHeader.module.css b/components/WarningHeader.module.css new file mode 100644 index 00000000..277d3dac --- /dev/null +++ b/components/WarningHeader.module.css @@ -0,0 +1,12 @@ +.warning{ + background-color: var(--warning-background); + padding: calc(6rem / 16); + color: var(--warning-text); + font-size: calc(16rem / 16); + text-align: center; + opacity: 1; + + /* The following are for a smooth fade in if there ever is a loading required for the warning, is not needed currently */ + /* max-height: 500px; + /* transition: max-height 1000ms ease-in, padding 100ms ease-in; */ +} \ No newline at end of file diff --git a/components/WarningHeader.tsx b/components/WarningHeader.tsx new file mode 100644 index 00000000..c8b0b632 --- /dev/null +++ b/components/WarningHeader.tsx @@ -0,0 +1,60 @@ +import { parse } from "date-fns"; +import React from "react"; + +import warnings from "../content/warnings/warnings.json"; +import { DATE_FORMAT, getLocalDateFromEST } from "../utils"; + +import styles from "./WarningHeader.module.css"; + +interface Warning { + message: string; + startDate: string; + endDate: string; +} + +function getCurrentWarning(): Warning | null { + const today = new Date(); + const currentWarnings: Warning[] = warnings.filter((warning) => { + // convert dates to date objects in EST time zone + let startDate = parse(warning.startDate, DATE_FORMAT, new Date()); + let endDate = parse(warning.endDate, DATE_FORMAT, new Date()); + + if ( + !startDate || + !endDate || + isNaN(startDate.getTime()) || // this checks if the parsed date is not valid (eg. wrong format), since getLocalDateFromEST fails with invalid dates + isNaN(endDate.getTime()) + ) { + throw new Error('WARNING WITH INVALID DATES: "' + warning.message + '"'); + } + + startDate = getLocalDateFromEST(startDate); + endDate = getLocalDateFromEST(endDate); + + return ( + startDate.getTime() <= today.getTime() && + endDate.getTime() >= today.getTime() + ); + }); + + if (currentWarnings.length > 1) { + // If more than one warning is scheduled, log an error to the console. We cannot throw an error, since the site would go down on the live + // website, on the day when more than one warning is scheduled. + console.error( + "ERROR: MORE THAN ONE WARNING SCHEDULED CURRENTLY! ", + currentWarnings + ); + } + + return currentWarnings.length === 0 ? null : currentWarnings[0]; +} + +export function WarningHeader() { + const warning = getCurrentWarning(); + + if (warning == null) { + return null; + } + + return
{warning.message}
; +} diff --git a/content/warnings/warnings.json b/content/warnings/warnings.json new file mode 100644 index 00000000..da46247d --- /dev/null +++ b/content/warnings/warnings.json @@ -0,0 +1,12 @@ +[ + { + "startDate": "February 15 2022 00:00", + "endDate": "February 20 2022 18:00", + "message": "Warning: There will be a scheduled system maintenance on February 17 from 9pm to 12pm EST" + }, + { + "startDate": "January 29 2022 21:00", + "endDate": "January 30 2022 18:00", + "message": "This is a sample warning" + } +] \ No newline at end of file diff --git a/lib/events.ts b/lib/events.ts index 06952eb3..76432930 100644 --- a/lib/events.ts +++ b/lib/events.ts @@ -2,14 +2,19 @@ import fs from "fs/promises"; import path from "path"; import { parse } from "date-fns"; -import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; import matter from "gray-matter"; import { MDXRemoteSerializeResult } from "next-mdx-remote"; import { serialize } from "next-mdx-remote/serialize"; import type { Props } from "../pages/events/[year]/[term]/index"; // do not use alias "@/utils" as generate-calendar imports a function from this file and ts-node is not compatible -import { Term, TERMS, isTerm } from "../utils"; +import { + Term, + TERMS, + isTerm, + DATE_FORMAT, + getLocalDateFromEST, +} from "../utils"; const EVENTS_PATH = path.join("content", "events"); @@ -55,8 +60,6 @@ export interface Event { metadata: Metadata; } -export const DATE_FORMAT = "MMMM dd yyyy HH:mm"; - export async function getEventBySlug( year: string, term: Term, @@ -284,12 +287,3 @@ function getFutureTerm(year: string, term: Term): { year: string; term: Term } { term: TERMS[index + 1], }; } - -// The date that's returned should be in local time -export function getLocalDateFromEST(date: Date) { - return utcToZonedTime( - // The parsed date is in EST - zonedTimeToUtc(date, "America/Toronto"), - Intl.DateTimeFormat().resolvedOptions().timeZone - ); -} diff --git a/lib/news.ts b/lib/news.ts index 6abd9547..69812ec5 100644 --- a/lib/news.ts +++ b/lib/news.ts @@ -8,7 +8,7 @@ import { serialize } from "next-mdx-remote/serialize"; import { isTerm, Term, TERMS } from "@/utils"; -import { DATE_FORMAT, getLocalDateFromEST } from "./events"; +import { DATE_FORMAT, getLocalDateFromEST } from "../utils"; export const NEWS_PATH = path.join("content", "news"); diff --git a/pages/_app.css b/pages/_app.css index 92507f49..a1cfdb86 100644 --- a/pages/_app.css +++ b/pages/_app.css @@ -22,6 +22,8 @@ body { --text: #000000; --form-invalid: #9f616a; + --warning-background: #dd0014; + --warning-text: #ffffff; --input-background: #f0f0f0; --input-placeholder-text: #bbbbbb; diff --git a/pages/_app.tsx b/pages/_app.tsx index e33deb74..c166ece4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -20,6 +20,7 @@ import { } from "@/components/ShapesBackground"; import { Table } from "@/components/Table"; import { ThemeProvider } from "@/components/Theme"; +import { WarningHeader } from "@/components/WarningHeader"; import styles from "./_app.module.css"; @@ -44,6 +45,7 @@ export default function App({ Component, pageProps }: AppProps): JSX.Element { }} >
+ {/* Wrapping content with a div to allow for a display: block parent */}
diff --git a/scripts/change-dates.ts b/scripts/change-dates.ts index a4a112d4..a6dacef6 100644 --- a/scripts/change-dates.ts +++ b/scripts/change-dates.ts @@ -3,8 +3,9 @@ import path from "path"; import { format } from "date-fns"; +import { DATE_FORMAT } from "@/utils"; + import { - DATE_FORMAT, getEventsByTerm, getEventTermsByYear, getEventYears, diff --git a/utils.ts b/utils.ts index fc08499a..0df41524 100644 --- a/utils.ts +++ b/utils.ts @@ -1,5 +1,8 @@ +import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; + export const TERMS = ["winter", "spring", "fall"] as const; export type Term = typeof TERMS[number]; +export const DATE_FORMAT = "MMMM dd yyyy HH:mm"; // https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates export function isTerm(x: string): x is Term { @@ -9,3 +12,12 @@ export function isTerm(x: string): x is Term { export function capitalize(str: string) { return str.slice(0, 1).toUpperCase() + str.slice(1); } + +// Converts a date to local time +export function getLocalDateFromEST(date: Date): Date { + return utcToZonedTime( + // The date parameter is in EST + zonedTimeToUtc(date, "America/Toronto"), + Intl.DateTimeFormat().resolvedOptions().timeZone + ); +}