From d9a6cfd0abe602ffd8d1768ac4845a978e529ee2 Mon Sep 17 00:00:00 2001 From: Cyrus Yiu Date: Sat, 16 Mar 2024 21:42:20 -0400 Subject: [PATCH 1/2] Environment page updated --- src/components/Adsense/adsense.tsx | 2 +- src/components/Analytics/analytics.tsx | 2 +- src/components/FeatureFlags/featureFlags.ts | 2 +- src/components/TextCountFromDate/index.tsx | 67 +++++++++++++++++++ src/components/WithAppProps/appProps.ts | 44 +++++------- src/components/WithAppProps/index.ts | 2 +- .../environment.tsx => _environment.tsx} | 33 ++++++--- src/pages/_environment/index.ts | 1 - src/scripts/Database/database.ts | 4 +- src/scripts/RSS/index.ts | 2 +- src/scripts/SiteWebmanifest/manifest.ts | 2 +- src/scripts/Utils/DateAndTime/Format.ts | 20 +++++- src/scripts/Utils/Environment/index.tsx | 23 +++++++ 13 files changed, 153 insertions(+), 51 deletions(-) create mode 100644 src/components/TextCountFromDate/index.tsx rename src/pages/{_environment/environment.tsx => _environment.tsx} (52%) delete mode 100644 src/pages/_environment/index.ts create mode 100644 src/scripts/Utils/Environment/index.tsx diff --git a/src/components/Adsense/adsense.tsx b/src/components/Adsense/adsense.tsx index d3e9a6e..fd1034d 100644 --- a/src/components/Adsense/adsense.tsx +++ b/src/components/Adsense/adsense.tsx @@ -1,6 +1,6 @@ import { GoogleAdSense } from "nextjs-google-adsense"; import React from "react"; -import { getEnvironment } from "../WithAppProps"; +import { getEnvironment } from "@/scripts/Utils/Environment"; export function Adsense(): JSX.Element { React.useEffect(() => { diff --git a/src/components/Analytics/analytics.tsx b/src/components/Analytics/analytics.tsx index c2af6ed..0c160c2 100644 --- a/src/components/Analytics/analytics.tsx +++ b/src/components/Analytics/analytics.tsx @@ -1,7 +1,7 @@ import type { NextWebVitalsMetric } from "next/app"; import { event, GoogleAnalytics } from "nextjs-google-analytics"; import React from "react"; -import { getEnvironment } from "@/components/WithAppProps"; +import { getEnvironment } from "@/scripts/Utils/Environment"; export function getAdStorageConsent(): string { return window.localStorage.getItem("adStorageConsent") || "denied"; diff --git a/src/components/FeatureFlags/featureFlags.ts b/src/components/FeatureFlags/featureFlags.ts index b287061..f9333cc 100644 --- a/src/components/FeatureFlags/featureFlags.ts +++ b/src/components/FeatureFlags/featureFlags.ts @@ -1,6 +1,6 @@ import { GrowthBook } from "@growthbook/growthbook-react"; -import { getEnvironment } from "@/components/WithAppProps"; import { AnalyticEvents } from "@/components/Analytics"; +import { getEnvironment } from "@/scripts/Utils/Environment"; export const growthbook = new GrowthBook({ apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST, diff --git a/src/components/TextCountFromDate/index.tsx b/src/components/TextCountFromDate/index.tsx new file mode 100644 index 0000000..c36a22b --- /dev/null +++ b/src/components/TextCountFromDate/index.tsx @@ -0,0 +1,67 @@ +import { formatDuration } from "@/scripts/Utils/DateAndTime/Format"; +import React from "react"; + +export function TextCountdown({ + date, + updatePeriod, + onEnd, +}: { + date: Date; + updatePeriod?: number; + onEnd?: () => void; +}): React.ReactNode { + const [timeLeft, setTimeLeft] = React.useState( + date.getTime() - Date.now(), + ); + + React.useEffect(() => { + const interval = setInterval(() => { + setTimeLeft(date.getTime() - Date.now()); + if (timeLeft <= 0) { + clearInterval(interval); + if (onEnd) { + onEnd(); + } + } + }, updatePeriod ?? 100); + + return () => { + clearInterval(interval); + }; + }, [date, onEnd, timeLeft, updatePeriod]); + + return <>{timeLeft > 0 ? formatDuration(timeLeft) : null}; +} + +export function TextCountup({ + date, + updatePeriod, + serverRender = true, +}: { + date: Date; + updatePeriod?: number; + serverRender?: boolean; +}): React.ReactNode { + const [show, setShow] = React.useState(serverRender); + const [timeSince, setTimeSince] = React.useState( + Date.now() - date.getTime(), + ); + + React.useEffect(() => { + const interval = setInterval(() => { + setTimeSince(Date.now() - date.getTime()); + }, updatePeriod ?? 100); + + return () => { + clearInterval(interval); + }; + }, [date, updatePeriod]); + + React.useEffect(() => { + setShow(true); + }, []); + + return ( + <>{show ? (timeSince > 0 ? formatDuration(timeSince) : null) : null} + ); +} diff --git a/src/components/WithAppProps/appProps.ts b/src/components/WithAppProps/appProps.ts index 1f43296..22cf796 100644 --- a/src/components/WithAppProps/appProps.ts +++ b/src/components/WithAppProps/appProps.ts @@ -1,37 +1,14 @@ -import { parseExtensionXML, parseToolXML } from "../../scripts/ParseListXML"; +import { parseExtensionXML, parseToolXML } from "@/scripts/ParseListXML"; import { promises as fs } from "fs"; import path from "path"; - -type Environment = "development" | "preview" | "production"; - -export function getEnvironment(): Environment { - return process.env.VERCEL_ENV != undefined - ? (process.env.VERCEL_ENV as Environment) - : process.env.NODE_ENV != undefined - ? (process.env.NODE_ENV as Environment) - : "production"; -} - -export function getBaseURL(): string { - switch (getEnvironment()) { - case "production": { - return "https://awesome-arcade.vercel.app"; - } - case "preview": { - return "https://awesome-arcade-beta.vercel.app"; - } - case "development": { - return "http://localhost:3000"; - } - } -} - -export function getBranch(): "main" | "staging" { - return getEnvironment() === "production" ? "main" : "staging"; -} +import { Environment, getEnvironment } from "@/scripts/Utils/Environment"; +import { execSync } from "node:child_process"; export interface AppProps { environment: Environment; + buildHash: string; + buildBranch: string; + buildTime: string; extensionsListed: number; toolsListed: number; } @@ -39,6 +16,15 @@ export interface AppProps { export async function getAppProps(): Promise { return { environment: getEnvironment(), + buildHash: + process.env.VERCEL_GIT_COMMIT_SHA != undefined + ? process.env.VERCEL_GIT_COMMIT_SHA + : execSync("git rev-parse HEAD").toString().trim(), + buildBranch: + process.env.VERCEL_GIT_COMMIT_REF != undefined + ? process.env.VERCEL_GIT_COMMIT_REF + : execSync("git rev-parse --abbrev-ref HEAD").toString().trim(), + buildTime: new Date().toISOString(), extensionsListed: ( await parseExtensionXML( ( diff --git a/src/components/WithAppProps/index.ts b/src/components/WithAppProps/index.ts index 0cbf7bd..23b5d42 100644 --- a/src/components/WithAppProps/index.ts +++ b/src/components/WithAppProps/index.ts @@ -1,2 +1,2 @@ -export { default, getEnvironment, getBranch } from "./appProps"; +export { default } from "./appProps"; export type { AppProps } from "./appProps"; diff --git a/src/pages/_environment/environment.tsx b/src/pages/_environment.tsx similarity index 52% rename from src/pages/_environment/environment.tsx rename to src/pages/_environment.tsx index 55bb1f1..53c5d47 100644 --- a/src/pages/_environment/environment.tsx +++ b/src/pages/_environment.tsx @@ -1,20 +1,33 @@ import React from "react"; -import Layout from "../../components/Layout"; -import getAppProps, { AppProps } from "../../components/WithAppProps"; +import Layout from "../components/Layout"; +import getAppProps, { AppProps } from "../components/WithAppProps"; +import { formatDateAndTimeAndSecond } from "@/scripts/Utils/DateAndTime/Format"; +import { TextCountup } from "@/components/TextCountFromDate"; -const pageName = "Environment build variables"; +const pageName = "Environment"; interface AboutProps { - vercelVars: string[][]; + envVars: string[][]; appProps: AppProps; } -export function About({ vercelVars, appProps }: AboutProps): JSX.Element { +export function About({ envVars, appProps }: AboutProps): JSX.Element { return ( -

Environment build variables

-
-        {vercelVars.map((value: string[]) => {
+      

Environment

+

+ Build hash: {appProps.buildHash} +
+ Build branch: {appProps.buildBranch} +
+ Build time: {appProps.buildTime} ( + {formatDateAndTimeAndSecond(new Date(appProps.buildTime))} -{" "} + {" "} + ago) +

+

Build variables

+ + {envVars.map((value: string[]) => { return ( {value[0]}: {value[1]} @@ -22,7 +35,7 @@ export function About({ vercelVars, appProps }: AboutProps): JSX.Element { ); })} -
+
); } @@ -45,7 +58,7 @@ export async function getStaticProps(): Promise<{ props: AboutProps }> { return { props: { - vercelVars: vercelEnvs.map((envVar: string) => { + envVars: vercelEnvs.map((envVar: string) => { return [ envVar, process.env[envVar] != undefined ? process.env[envVar] : "undefined", diff --git a/src/pages/_environment/index.ts b/src/pages/_environment/index.ts deleted file mode 100644 index 2e5a052..0000000 --- a/src/pages/_environment/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default, getStaticProps } from "./environment"; diff --git a/src/scripts/Database/database.ts b/src/scripts/Database/database.ts index d03cd07..97c9e69 100644 --- a/src/scripts/Database/database.ts +++ b/src/scripts/Database/database.ts @@ -1,9 +1,9 @@ import { Pool, QueryConfig, QueryResult } from "pg"; -import { getEnvironment } from "@/components/WithAppProps"; +import { getEnvironment } from "@/scripts/Utils/Environment"; export default function queryDb( queryTextOrConfig: string | QueryConfig, - values?: any[] | undefined + values?: any[] | undefined, ): Promise> { const pool = new Pool({ host: process.env.POSTGRES_HOST, diff --git a/src/scripts/RSS/index.ts b/src/scripts/RSS/index.ts index d987b67..3ae2b2d 100644 --- a/src/scripts/RSS/index.ts +++ b/src/scripts/RSS/index.ts @@ -1,6 +1,6 @@ import { Feed } from "feed"; -import { getBaseURL, getEnvironment } from "@/components/WithAppProps/appProps"; import { BlogPostPreview } from "@/components/Blog/Post/Preview"; +import { getBaseURL, getEnvironment } from "@/scripts/Utils/Environment"; export default async function generateRSSFeed( posts: BlogPostPreview[], diff --git a/src/scripts/SiteWebmanifest/manifest.ts b/src/scripts/SiteWebmanifest/manifest.ts index 3a875e8..7dbc348 100644 --- a/src/scripts/SiteWebmanifest/manifest.ts +++ b/src/scripts/SiteWebmanifest/manifest.ts @@ -1,4 +1,4 @@ -import { getEnvironment } from "../../components/WithAppProps"; +import { getEnvironment } from "@/scripts/Utils/Environment"; export default async function generateSiteWebmanifest(): Promise { const json = JSON.parse(`{ diff --git a/src/scripts/Utils/DateAndTime/Format.ts b/src/scripts/Utils/DateAndTime/Format.ts index 29838c8..a80c0fd 100644 --- a/src/scripts/Utils/DateAndTime/Format.ts +++ b/src/scripts/Utils/DateAndTime/Format.ts @@ -28,16 +28,30 @@ export function formatDateAndTime(date: Date) { }); } -export function formatDuration(ms: number) { +export function formatDateAndTimeAndSecond(date: Date) { + const locale = new Intl.NumberFormat().resolvedOptions().locale; + return date.toLocaleTimeString(locale, { + month: "long", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }); +} + +export function formatDuration(ms: number, showMilliseconds = false) { // https://www.30secondsofcode.org/js/s/number-formatting/ if (ms < 0) ms = -ms; - const time = { + let time: { [unit: string]: number } = { day: Math.floor(ms / 86_400_000), hour: Math.floor(ms / 3_600_000) % 24, minute: Math.floor(ms / 60_000) % 60, second: Math.floor(ms / 1_000) % 60, - millisecond: Math.floor(ms) % 1_000, }; + if (showMilliseconds) { + time.millisecond = Math.floor(ms) % 1_000; + } return Object.entries(time) .filter((val) => val[1] !== 0) .map(([key, val]) => `${val} ${key}${val !== 1 ? "s" : ""}`) diff --git a/src/scripts/Utils/Environment/index.tsx b/src/scripts/Utils/Environment/index.tsx new file mode 100644 index 0000000..31b3073 --- /dev/null +++ b/src/scripts/Utils/Environment/index.tsx @@ -0,0 +1,23 @@ +export type Environment = "development" | "preview" | "production"; + +export function getEnvironment(): Environment { + return process.env.VERCEL_ENV != undefined + ? (process.env.VERCEL_ENV as Environment) + : process.env.NODE_ENV != undefined + ? (process.env.NODE_ENV as Environment) + : "production"; +} + +export function getBaseURL(): string { + switch (getEnvironment()) { + case "production": { + return "https://awesome-arcade.vercel.app"; + } + case "preview": { + return "https://awesome-arcade-beta.vercel.app"; + } + case "development": { + return "http://localhost:3000"; + } + } +} From 4a6812725f032f4421a228f0ed7571fc74923e65 Mon Sep 17 00:00:00 2001 From: Cyrus Yiu Date: Sat, 16 Mar 2024 22:30:39 -0400 Subject: [PATCH 2/2] Footer shows build hash, branch, and time --- src/components/Footer/footer.tsx | 24 ++++++++++++++++++++- src/components/TextCountFromDate/Static.tsx | 19 ++++++++++++++++ src/pages/_environment.tsx | 18 ++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/components/TextCountFromDate/Static.tsx diff --git a/src/components/Footer/footer.tsx b/src/components/Footer/footer.tsx index a6fd67c..561e552 100644 --- a/src/components/Footer/footer.tsx +++ b/src/components/Footer/footer.tsx @@ -5,7 +5,10 @@ import Image from "next/image"; import { appName } from "@/components/Layout/layout"; import icon from "../../../public/android-chrome-512x512.png"; import { AppProps } from "@/components/WithAppProps"; -import { formatDateLong } from "@/scripts/Utils/DateAndTime/Format"; +import { + formatDateAndTime, + formatDateLong, +} from "@/scripts/Utils/DateAndTime/Format"; export const DEVELOPERS = ["UnsignedArduino"]; export const CREATION_DATE = new Date("2023-04-15T02:00:20Z"); @@ -113,6 +116,25 @@ function Footer({ appProps }: { appProps: AppProps }): JSX.Element { owner of MakeCode Arcade.
Microsoft and MakeCode Arcade are trademarks of the Microsoft group of companies. +
+
+ Build{" "} + + {appProps.buildHash} + {" "} + (branch{" "} + + {appProps.buildBranch} + + ) on {formatDateAndTime(new Date(appProps.buildTime))}. ); diff --git a/src/components/TextCountFromDate/Static.tsx b/src/components/TextCountFromDate/Static.tsx new file mode 100644 index 0000000..87c0ed1 --- /dev/null +++ b/src/components/TextCountFromDate/Static.tsx @@ -0,0 +1,19 @@ +import { formatDuration } from "@/scripts/Utils/DateAndTime/Format"; +import React from "react"; + +export function StaticLowPrecisionTextCountup({ + date, +}: { + date: Date; +}): React.ReactNode { + const [show, setShow] = React.useState(false); + const timeSince = Date.now() - date.getTime(); + + React.useEffect(() => { + setShow(true); + }, []); + + return ( + <>{show ? (timeSince > 0 ? formatDuration(timeSince) : null) : null} + ); +} diff --git a/src/pages/_environment.tsx b/src/pages/_environment.tsx index 53c5d47..159b5ab 100644 --- a/src/pages/_environment.tsx +++ b/src/pages/_environment.tsx @@ -16,9 +16,23 @@ export function About({ envVars, appProps }: AboutProps): JSX.Element {

Environment

- Build hash: {appProps.buildHash} + Build hash:{" "} + + {appProps.buildHash} +
- Build branch: {appProps.buildBranch} + Build branch:{" "} + + {appProps.buildBranch} +
Build time: {appProps.buildTime} ( {formatDateAndTimeAndSecond(new Date(appProps.buildTime))} -{" "}