Add October Today route
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { OctoberCounter } from "~/components/OctoberCounter";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "October Today | Sean O'Connor",
|
||||||
|
description: "See what day of October 2019 it is today.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OctoberPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[calc(100vh-12rem)] items-center justify-center py-8">
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<OctoberCounter />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { CircleHelp, Share2 } from "lucide-react";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
|
||||||
|
const OCTOBER_FIRST_2019 = Date.UTC(2019, 9, 1);
|
||||||
|
const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
|
function getOrdinalSuffix(number: number) {
|
||||||
|
if (number % 100 >= 11 && number % 100 <= 13) {
|
||||||
|
return "th";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (number % 10) {
|
||||||
|
case 1:
|
||||||
|
return "st";
|
||||||
|
case 2:
|
||||||
|
return "nd";
|
||||||
|
case 3:
|
||||||
|
return "rd";
|
||||||
|
default:
|
||||||
|
return "th";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOctoberDay(today: Date) {
|
||||||
|
const todayUtc = Date.UTC(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth(),
|
||||||
|
today.getDate(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Math.floor((todayUtc - OCTOBER_FIRST_2019) / MILLISECONDS_PER_DAY) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OctoberCounter() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const isJsonMode = searchParams.get("api") === "json";
|
||||||
|
const [targetDay] = useState(() => getOctoberDay(new Date()));
|
||||||
|
const [day, setDay] = useState(() =>
|
||||||
|
isJsonMode ? targetDay : Math.max(1, targetDay - 50),
|
||||||
|
);
|
||||||
|
const isAnimating = !isJsonMode && day < targetDay;
|
||||||
|
const ordinal = getOrdinalSuffix(targetDay);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isJsonMode) return;
|
||||||
|
|
||||||
|
let currentNumber = Math.max(1, targetDay - 50);
|
||||||
|
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
currentNumber += 1;
|
||||||
|
setDay(currentNumber);
|
||||||
|
|
||||||
|
if (currentNumber >= targetDay) {
|
||||||
|
window.clearInterval(interval);
|
||||||
|
document.title = `October ${targetDay}${ordinal}, 2019`;
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
return () => window.clearInterval(interval);
|
||||||
|
}, [isJsonMode, ordinal, targetDay]);
|
||||||
|
|
||||||
|
if (isJsonMode) {
|
||||||
|
const response = {
|
||||||
|
day,
|
||||||
|
ordinal,
|
||||||
|
formatted: `${day}${ordinal}`,
|
||||||
|
text: `happy october ${day}${ordinal}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre className="w-full max-w-xl overflow-x-auto rounded-2xl bg-muted p-6 font-mono text-sm text-foreground ring-1 ring-foreground/10">
|
||||||
|
{JSON.stringify(response, null, 2)}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShare = () => {
|
||||||
|
const message = `happy october ${day}${ordinal}`;
|
||||||
|
window.open(`sms:?&body=${encodeURIComponent(message)}`, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="group relative z-10 mx-auto w-full max-w-[600px] overflow-hidden rounded-3xl border-none bg-background/80 py-0 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-xl">
|
||||||
|
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-transparent via-white/5 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
||||||
|
<CardContent className="p-8 text-center sm:p-12">
|
||||||
|
<h1 className="mb-8 text-2xl font-medium tracking-tight sm:text-[2rem]">
|
||||||
|
Today is
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="my-8" aria-live="polite" aria-busy={isAnimating}>
|
||||||
|
<h2 className="mb-1 font-heading text-[2.5rem] leading-none tracking-tight sm:text-[3.5rem]">
|
||||||
|
October{" "}
|
||||||
|
<span className="relative inline-block font-bold text-primary">
|
||||||
|
{day}
|
||||||
|
<span className="absolute -bottom-1 left-0 h-[3px] w-full origin-left scale-x-0 bg-primary transition-transform duration-300 ease-out group-hover:scale-x-100" />
|
||||||
|
</span>
|
||||||
|
<sup className="text-[0.6em]">{isAnimating ? "th" : ordinal}</sup>
|
||||||
|
</h2>
|
||||||
|
<p className="text-2xl tracking-tight text-muted-foreground">2019.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col justify-center gap-3 sm:flex-row sm:gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleShare}
|
||||||
|
className="rounded-xl transition-transform duration-200 hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<Share2 />
|
||||||
|
Share via SMS
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-xl transition-transform duration-200 hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<CircleHelp />
|
||||||
|
Why?
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user