From 766839ecef849f2ea8298aaf5cbaf4823c0a4c7d Mon Sep 17 00:00:00 2001 From: wangharold001 Date: Mon, 22 Sep 2025 02:42:39 -0700 Subject: [PATCH] Add configurable application deadline countdown --- README.md | 6 + deadline-update.diff | 248 +++++++++++++++++++++ src/components/banner/index.tsx | 35 ++- src/config/applicationDeadline.ts | 47 ++++ src/sections/Hero/index.tsx | 11 +- src/sections/Photo-Gallery/NextJsImage.tsx | 17 +- src/sections/description/index.tsx | 6 +- 7 files changed, 351 insertions(+), 19 deletions(-) create mode 100644 deadline-update.diff create mode 100644 src/config/applicationDeadline.ts diff --git a/README.md b/README.md index 52bf3e0..ec8543d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +## Configuration + +### Application deadline + +The homepage countdown and hero call-to-action read the upcoming application deadline from the `NEXT_PUBLIC_APPLICATION_DEADLINE` environment variable. Provide this value in ISO-8601 format (for example, `2025-10-03T23:59:59-07:00` for October 3, 2025 at 11:59 PM PT). When the variable is omitted or invalid, the site falls back to that same October 3 deadline. + ## Learn More To learn more about Next.js, take a look at the following resources: diff --git a/deadline-update.diff b/deadline-update.diff new file mode 100644 index 0000000..8a29a5e --- /dev/null +++ b/deadline-update.diff @@ -0,0 +1,248 @@ +diff --git a/README.md b/README.md +index 52bf3e0..ec8543d 100644 +--- a/README.md ++++ b/README.md +@@ -14,6 +14,12 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update + This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + ++## Configuration ++ ++### Application deadline ++ ++The homepage countdown and hero call-to-action read the upcoming application deadline from the `NEXT_PUBLIC_APPLICATION_DEADLINE` environment variable. Provide this value in ISO-8601 format (for example, `2025-10-03T23:59:59-07:00` for October 3, 2025 at 11:59 PM PT). When the variable is omitted or invalid, the site falls back to that same October 3 deadline. ++ + ## Learn More + + To learn more about Next.js, take a look at the following resources: + +diff --git a/src/components/banner/index.tsx b/src/components/banner/index.tsx +index 9ed6fcc..d74126b 100644 +--- a/src/components/banner/index.tsx ++++ b/src/components/banner/index.tsx +@@ -1,6 +1,10 @@ + "use client"; + +-import { useState, useEffect } from "react"; ++import { useState, useEffect, useMemo } from "react"; ++import { ++ applicationDeadline, ++ isUpcomingDeadline, ++} from "@/src/config/applicationDeadline"; + import styles from "./style.module.scss"; + + const Banner = () => { +@@ -8,11 +12,17 @@ const Banner = () => { + const [hours, setHours] = useState(0); + const [minutes, setMinutes] = useState(0); + const [seconds, setSeconds] = useState(0); ++ const [isExpired, setIsExpired] = useState(() => !isUpcomingDeadline()); ++ ++ const target = useMemo(() => applicationDeadline, []); + + useEffect(() => { +- const target = new Date("04/02/2024 23:59:59"); ++ if (!target) { ++ setIsExpired(true); ++ return; ++ } + +- const interval = setInterval(() => { ++ const updateCountdown = () => { + const now = new Date(); + const difference = target.getTime() - now.getTime(); + +@@ -22,7 +32,7 @@ const Banner = () => { + setHours(0); + setMinutes(0); + setSeconds(0); +- clearInterval(interval); ++ setIsExpired(true); + } else { + const d = Math.floor(difference / (1000 * 60 * 60 * 24)); + setDays(d); +@@ -37,19 +47,28 @@ const Banner = () => { + + const s = Math.floor((difference % (1000 * 60)) / 1000); + setSeconds(s); ++ setIsExpired(false); + } +- }, 1000); ++ }; ++ ++ updateCountdown(); ++ ++ const interval = setInterval(updateCountdown, 1000); + + return () => clearInterval(interval); +- }, []); ++ }, [target]); ++ ++ if (!target || isExpired) { ++ return null; ++ } + + return ( +
+
+ Projects applications close in{" "} +- ++ + {days} days {hours} hours {minutes} minutes {seconds} seconds +- ++ +
+
+ ); + +diff --git a/src/config/applicationDeadline.ts b/src/config/applicationDeadline.ts +new file mode 100644 +index 0000000..0100ff9 +--- /dev/null ++++ b/src/config/applicationDeadline.ts +@@ -0,0 +1,43 @@ ++const FALLBACK_DEADLINE = "2025-10-03T23:59:59-07:00"; ++const DEADLINE_TIME_ZONE = "America/Los_Angeles"; ++ ++const parseDeadline = (raw?: string | null): Date | null => { ++ if (!raw) return null; ++ ++ const parsed = new Date(raw); ++ return Number.isNaN(parsed.getTime()) ? null : parsed; ++}; ++ ++const resolveDeadline = (): Date | null => { ++ const fromEnv = parseDeadline( ++ process.env.NEXT_PUBLIC_APPLICATION_DEADLINE ?? null ++ ); ++ ++ if (fromEnv) return fromEnv; ++ ++ return parseDeadline(FALLBACK_DEADLINE); ++}; ++ ++const formatDeadline = (deadline: Date | null): string | null => { ++ if (!deadline) return null; ++ ++ try { ++ return new Intl.DateTimeFormat("en-US", { ++ month: "long", ++ day: "numeric", ++ hour: "numeric", ++ minute: "2-digit", ++ hour12: true, ++ timeZone: DEADLINE_TIME_ZONE, ++ timeZoneName: "short", ++ }).format(deadline); ++ } catch (error) { ++ return deadline.toLocaleString(); ++ } ++}; ++ ++export const applicationDeadline: Date | null = resolveDeadline(); ++export const formattedApplicationDeadline: string | null = ++ formatDeadline(applicationDeadline); ++ ++export const isUpcomingDeadline = (reference = new Date()): boolean => { ++ return !!( ++ applicationDeadline && applicationDeadline.getTime() > reference.getTime() ++ ); ++}; + +diff --git a/src/sections/Hero/index.tsx b/src/sections/Hero/index.tsx +index 6b8d483..0ff44a6 100644 +--- a/src/sections/Hero/index.tsx ++++ b/src/sections/Hero/index.tsx +@@ -1,10 +1,17 @@ + "use client"; +-import Image from "next/image"; + import styles from "./style.module.scss"; + import Description from "../description"; ++import { ++ formattedApplicationDeadline, ++ isUpcomingDeadline, ++} from "@/src/config/applicationDeadline"; + + const Hero = () => { + const projects_app = "https://acmurl.com/projects-app"; ++ const hasUpcomingDeadline = isUpcomingDeadline(); ++ const applicationCopy = hasUpcomingDeadline ++ ? `Applications Due ${formattedApplicationDeadline}` ++ : "Applications currently closed"; + + return ( +
+@@ -34,7 +41,7 @@ const Hero = () => { + + +
+- Applications Due April 2nd, 11:59PM! ++ {applicationCopy} +
+
+ + +diff --git a/src/sections/Photo-Gallery/NextJsImage.tsx b/src/sections/Photo-Gallery/NextJsImage.tsx +index d2cc0d4..6de3dd6 100644 +--- a/src/sections/Photo-Gallery/NextJsImage.tsx ++++ b/src/sections/Photo-Gallery/NextJsImage.tsx +@@ -1,21 +1,26 @@ +-import Image from "next/image"; + import type { RenderPhotoProps } from "react-photo-album"; +-import { CldImage } from 'next-cloudinary'; +-import s from "./style.module.scss" ++import { CldImage } from "next-cloudinary"; ++ ++import s from "./style.module.scss"; + + export default function NextJsImage({ + photo, + imageProps: { alt, title, sizes, className, onClick }, + wrapperStyle, + }: RenderPhotoProps) { ++ const combinedClassName = className ? `${className} ${s.image}` : s.image; ++ + return ( +
+ +
+ ); + +diff --git a/src/sections/description/index.tsx b/src/sections/description/index.tsx +index ad3f286..359f098 100644 +--- a/src/sections/description/index.tsx ++++ b/src/sections/description/index.tsx +@@ -6,18 +6,18 @@ const Description = () => { + return ( +
+
+- AI projects focus on building a project ++ AI projects focus on building a project + related to all things AI, from natural language processing to computer + vision and more!{" "} +
+
+- Hack projects works to build a full MERN ++ Hack projects works to build a full MERN + stack website, emulating a software engineering team working on the + Agile process! +
+ +
+- Design projects work on creating or ++ Design projects work on creating or + redesigning a platform, working through the design process from research + to prototyping and more! +
+ diff --git a/src/components/banner/index.tsx b/src/components/banner/index.tsx index 9ed6fcc..d74126b 100644 --- a/src/components/banner/index.tsx +++ b/src/components/banner/index.tsx @@ -1,6 +1,10 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; +import { + applicationDeadline, + isUpcomingDeadline, +} from "@/src/config/applicationDeadline"; import styles from "./style.module.scss"; const Banner = () => { @@ -8,11 +12,17 @@ const Banner = () => { const [hours, setHours] = useState(0); const [minutes, setMinutes] = useState(0); const [seconds, setSeconds] = useState(0); + const [isExpired, setIsExpired] = useState(() => !isUpcomingDeadline()); + + const target = useMemo(() => applicationDeadline, []); useEffect(() => { - const target = new Date("04/02/2024 23:59:59"); + if (!target) { + setIsExpired(true); + return; + } - const interval = setInterval(() => { + const updateCountdown = () => { const now = new Date(); const difference = target.getTime() - now.getTime(); @@ -22,7 +32,7 @@ const Banner = () => { setHours(0); setMinutes(0); setSeconds(0); - clearInterval(interval); + setIsExpired(true); } else { const d = Math.floor(difference / (1000 * 60 * 60 * 24)); setDays(d); @@ -37,19 +47,28 @@ const Banner = () => { const s = Math.floor((difference % (1000 * 60)) / 1000); setSeconds(s); + setIsExpired(false); } - }, 1000); + }; + + updateCountdown(); + + const interval = setInterval(updateCountdown, 1000); return () => clearInterval(interval); - }, []); + }, [target]); + + if (!target || isExpired) { + return null; + } return (
Projects applications close in{" "} - + {days} days {hours} hours {minutes} minutes {seconds} seconds - +
); diff --git a/src/config/applicationDeadline.ts b/src/config/applicationDeadline.ts new file mode 100644 index 0000000..bcfeb29 --- /dev/null +++ b/src/config/applicationDeadline.ts @@ -0,0 +1,47 @@ +const FALLBACK_DEADLINE = "2025-10-03T23:59:59-07:00"; +const DEADLINE_TIME_ZONE = "America/Los_Angeles"; + +const parseDeadline = (raw?: string | null): Date | null => { + if (!raw) return null; + + const parsed = new Date(raw); + return Number.isNaN(parsed.getTime()) ? null : parsed; +}; + +const resolveDeadline = (): Date | null => { + const fromEnv = parseDeadline( + process.env.NEXT_PUBLIC_APPLICATION_DEADLINE ?? null + ); + + if (fromEnv) return fromEnv; + + return parseDeadline(FALLBACK_DEADLINE); +}; + +const formatDeadline = (deadline: Date | null): string | null => { + if (!deadline) return null; + + try { + return new Intl.DateTimeFormat("en-US", { + month: "long", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + timeZone: DEADLINE_TIME_ZONE, + timeZoneName: "short", + }).format(deadline); + } catch (error) { + return deadline.toLocaleString(); + } +}; + +export const applicationDeadline: Date | null = resolveDeadline(); +export const formattedApplicationDeadline: string | null = + formatDeadline(applicationDeadline); + + export const isUpcomingDeadline = (reference = new Date()): boolean => { + return !!( + applicationDeadline && applicationDeadline.getTime() > reference.getTime() + ); + }; \ No newline at end of file diff --git a/src/sections/Hero/index.tsx b/src/sections/Hero/index.tsx index 6b8d483..0ff44a6 100644 --- a/src/sections/Hero/index.tsx +++ b/src/sections/Hero/index.tsx @@ -1,10 +1,17 @@ "use client"; -import Image from "next/image"; import styles from "./style.module.scss"; import Description from "../description"; +import { + formattedApplicationDeadline, + isUpcomingDeadline, +} from "@/src/config/applicationDeadline"; const Hero = () => { const projects_app = "https://acmurl.com/projects-app"; + const hasUpcomingDeadline = isUpcomingDeadline(); + const applicationCopy = hasUpcomingDeadline + ? `Applications Due ${formattedApplicationDeadline}` + : "Applications currently closed"; return (
@@ -34,7 +41,7 @@ const Hero = () => {
- Applications Due April 2nd, 11:59PM! + {applicationCopy}
diff --git a/src/sections/Photo-Gallery/NextJsImage.tsx b/src/sections/Photo-Gallery/NextJsImage.tsx index d2cc0d4..6de3dd6 100644 --- a/src/sections/Photo-Gallery/NextJsImage.tsx +++ b/src/sections/Photo-Gallery/NextJsImage.tsx @@ -1,21 +1,26 @@ -import Image from "next/image"; import type { RenderPhotoProps } from "react-photo-album"; -import { CldImage } from 'next-cloudinary'; -import s from "./style.module.scss" +import { CldImage } from "next-cloudinary"; + +import s from "./style.module.scss"; export default function NextJsImage({ photo, imageProps: { alt, title, sizes, className, onClick }, wrapperStyle, }: RenderPhotoProps) { + const combinedClassName = className ? `${className} ${s.image}` : s.image; + return (
); diff --git a/src/sections/description/index.tsx b/src/sections/description/index.tsx index ad3f286..359f098 100644 --- a/src/sections/description/index.tsx +++ b/src/sections/description/index.tsx @@ -6,18 +6,18 @@ const Description = () => { return (
- AI projects focus on building a project + AI projects focus on building a project related to all things AI, from natural language processing to computer vision and more!{" "}
- Hack projects works to build a full MERN + Hack projects works to build a full MERN stack website, emulating a software engineering team working on the Agile process!
- Design projects work on creating or + Design projects work on creating or redesigning a platform, working through the design process from research to prototyping and more!