Skip to content
Merged
58 changes: 22 additions & 36 deletions components/layout/nav/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import moment from "moment";
import React from "react";
import { FaFacebook, FaHeart, FaInstagram, FaLinkedin, FaTiktok, FaXTwitter, FaYoutube } from "react-icons/fa6";
import { GitHubButtonWrapper } from "@/components/GitHubButtonWrapper";
import { pathPrefix } from "../../../site-config";
import { RelativeTime } from "./relative-time";

const buildTimestamp = process.env.BUILD_TIMESTAMP ? parseInt(process.env.BUILD_TIMESTAMP) : Date.now() - 1000 * 60 * 30;
const versionDeployed = process.env.VERSION_DEPLOYED || "dev";
const deploymentUrl = process.env.DEPLOYMENT_URL || "#";
const buildTimestamp = process.env.BUILD_TIMESTAMP ? parseInt(process.env.BUILD_TIMESTAMP, 10) : Date.now() - 1000 * 60 * 30;
const buildDate = process.env.BUILD_DATE;
const commitHash = process.env.COMMIT_HASH;

Expand Down Expand Up @@ -35,9 +33,9 @@ export const Footer = () => {
<footer className="text-center bg-[var(--footer-background)] text-[var(--footer-foreground)] py-6 md:py-4 lg:py-2 [&_a]:no-underline text-xs">
<section className="main-container max-w-screen-xl">
<div className="xl:mx-6">
<div className="mx-6 flex flex-col-reverse md:flex-row justify-between align-middle leading-6">
<div className="mx-6 flex flex-col-reverse lg:flex-row justify-between align-middle leading-6">
<div className="py-2">&copy; 1990-{new Date().getFullYear()} SSW. All rights reserved.</div>
<div className="w-full md:w-3/4 md:text-right py-2 flex max-sm:flex-col items-center max-sm:justify-start justify-center md:justify-end">
<div className="w-full lg:w-3/4 lg:text-right py-2 flex max-sm:flex-col items-center max-sm:justify-start justify-center lg:justify-end">
<a
className="inline-block text-white hover:text-ssw-red visited:text-white leading-3 transition-all duration-300 ease-in-out hover:text-ssw-red max-sm:mb-3"
href="https://github.com/SSWConsulting/SSW.Rules/issues"
Expand Down Expand Up @@ -121,28 +119,32 @@ export const Footer = () => {
</div>
</div>
<hr className="border-gray-800 my-2"></hr>
<div className="flex flex-col md:flex-row justify-between mx-6">
<div className="flex flex-col lg:flex-row justify-between mx-6">
<div className="py-2">
This website is under{" "}
<a
className="inline-block text-white hover:text-ssw-red visited:text-white leading-3 transition-all duration-300 ease-in-out hover:text-ssw-red"
href={`${pathPrefix}/rules-to-better-websites-deployment`}
>
CONSTANT CONTINUOUS DEPLOYMENT
continuous deployment
</a>
. Last deployed {getLastDeployTime()} ago
{buildDate && <span title={buildDate}> on {moment(buildDate).format("MMM D, YYYY [at] HH:mm UTC")}</span>} (Build #{" "}
<a
className="inline-block text-white hover:text-ssw-red visited:text-white leading-3 transition-all duration-300 ease-in-out hover:text-ssw-red"
href={deploymentUrl}
target="_blank"
rel="noopener noreferrer nofollow"
>
{versionDeployed}
</a>
{commitHash && <span title={`Commit: ${commitHash}`}>-{commitHash}</span>})
. Last updated{" "}
<RelativeTime buildTimestamp={buildTimestamp} buildDate={buildDate} />
{commitHash && (
<>
. Last commit{" "}
<a
className="inline-block text-white hover:text-ssw-red visited:text-white leading-3 transition-all duration-300 ease-in-out hover:text-ssw-red"
href={`https://github.com/SSWConsulting/SSW.Rules/commit/${commitHash}`}
target="_blank"
rel="noopener noreferrer nofollow"
>
{commitHash}
</a>
</>
)}
Comment thread
0xharkirat marked this conversation as resolved.
</div>
<div className="md:text-right py-2">
<div className="lg:text-right py-2">
Powered by{" "}
<a
className="inline-block text-white hover:text-ssw-red visited:text-white leading-3 transition-all duration-300 ease-in-out hover:text-ssw-red"
Expand All @@ -166,19 +168,3 @@ export const Footer = () => {
</>
);
};

const getLastDeployTime = () => {
const lastDeployDuration = moment.duration(Date.now() - buildTimestamp);
let delta = Math.abs(lastDeployDuration.asMilliseconds()) / 1000;

const days = Math.floor(delta / 86400);
delta -= days * 86400;

var hours = Math.floor(delta / 3600) % 24;
delta -= hours * 3600;

var minutes = Math.floor(delta / 60) % 60;
delta -= minutes * 60;

return days !== 0 ? `${days} day(s)` : hours !== 0 ? `${hours} hour(s)` : minutes > 1 ? `${minutes} minutes` : "1 minute";
};
90 changes: 90 additions & 0 deletions components/layout/nav/relative-time.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import { useEffect, useId, useState } from "react";

interface Props {
buildTimestamp: number;
buildDate?: string;
}

const MONTHS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];

function formatRelative(deltaMs: number): string {
const delta = Math.abs(deltaMs) / 1000;
const days = Math.floor(delta / 86400);
const hours = Math.floor((delta % 86400) / 3600);
const minutes = Math.floor((delta % 3600) / 60);
if (days > 0) return `${days} day${days > 1 ? "s" : ""} ago`;
if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ago`;
if (minutes > 1) return `${minutes} min ago`;
return "1 min ago";
}

function formatUtcDate(iso: string): string | undefined {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return undefined;
const day = d.getUTCDate();
const month = MONTHS[d.getUTCMonth()];
const year = d.getUTCFullYear();
const hh = String(d.getUTCHours()).padStart(2, "0");
const mm = String(d.getUTCMinutes()).padStart(2, "0");
return `${day} ${month} ${year} at ${hh}:${mm} UTC`;
}

/**
* Renders "X min/hour/day(s) ago" and ticks every 60s so the relative time
* stays accurate even when the page HTML is cached. On hover or keyboard
* focus it shows a custom tooltip with the exact build date (UTC). Client-
* only — server initially renders with delta=0 ("1 min ago") and the
* client immediately recomputes on mount to avoid hydration mismatch.
*
* Accessibility: the wrapper is focusable when a tooltip is present so
* keyboard users can reveal it, and the tooltip is linked via
* `aria-describedby` so screen readers announce it after the visible time.
*/
export function RelativeTime({ buildTimestamp, buildDate }: Props) {
const [now, setNow] = useState<number>(buildTimestamp);
const tooltipId = useId();

useEffect(() => {
setNow(Date.now());
const id = setInterval(() => setNow(Date.now()), 60_000);
return () => clearInterval(id);
}, []);

const formatted = buildDate ? formatUtcDate(buildDate) : undefined;
const tooltip = formatted ? `Last updated ${formatted}` : undefined;

Comment thread
0xharkirat marked this conversation as resolved.
return (
<span
className={`relative inline-block group text-white hover:text-ssw-red focus-visible:text-ssw-red transition-all duration-300 ease-in-out${tooltip ? " cursor-help" : ""}`}
tabIndex={tooltip ? 0 : undefined}
aria-describedby={tooltip ? tooltipId : undefined}
Comment thread
0xharkirat marked this conversation as resolved.
title={tooltip}
>
{formatRelative(now - buildTimestamp)}
{tooltip && (
<span
id={tooltipId}
role="tooltip"
className="absolute left-1/2 -translate-x-1/2 bottom-full mb-1 px-2 py-1 bg-white text-gray-900 text-[11px] leading-none rounded whitespace-nowrap opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 pointer-events-none transition-opacity duration-150 shadow-md z-10"
>
{tooltip}
</span>
)}
</span>
Comment thread
0xharkirat marked this conversation as resolved.
);
}
Loading