From 5261cfaa1a591d3f2da0ffd04bd184020875b5fa Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 12 Jul 2026 14:37:00 +0200 Subject: [PATCH] feat: add CMS info footer popup --- src/app/globals.css | 160 +++++++++++++++++++++++++ src/components/cms-info-popup.test.tsx | 10 ++ src/components/cms-info-popup.tsx | 56 +++++++++ src/components/site-footer.tsx | 4 +- 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/components/cms-info-popup.test.tsx create mode 100644 src/components/cms-info-popup.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 7d51803d..c36068d9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -776,3 +776,163 @@ tbody tr:hover { opacity: 0.6; } } + +/* Footer CMS info popup */ +.cms-info-trigger { + appearance: none; + border: 0; + background: transparent; + color: var(--link-color-readable, var(--link-color)); + cursor: pointer; + font: inherit; + font-size: 0.78rem; + font-weight: 800; + line-height: 1; + padding: 0.35rem 0.5rem; + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 3px; +} +.cms-info-trigger:hover, +.cms-info-trigger:focus-visible { + color: var(--link-hover-color-readable, var(--link-hover-color)); +} +.cms-info-trigger:focus-visible, +.cms-info-close:focus-visible { + outline: 3px solid color-mix(in srgb, var(--color-primary) 42%, transparent); + outline-offset: 3px; +} +.cms-info-backdrop { + position: fixed; + inset: 0; + z-index: 1000; + display: grid; + place-items: center; + padding: 1rem; + background: rgba(8, 10, 14, 0.68); + backdrop-filter: blur(6px); +} +.cms-info-dialog { + position: relative; + width: min(440px, 100%); + border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--border)); + border-radius: 8px; + background: + linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 12%, transparent), transparent 42%), + var(--color-surface); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.34); + color: var(--color-text-readable, var(--color-text)); + padding: 1.25rem; +} +.cms-info-close { + position: absolute; + top: 0.75rem; + right: 0.75rem; + display: grid; + width: 2rem; + height: 2rem; + place-items: center; + border: 1px solid color-mix(in srgb, var(--color-text-muted) 18%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--color-surface) 84%, transparent); + color: var(--color-text-muted-readable, var(--color-text-muted)); + cursor: pointer; + font-weight: 900; + line-height: 1; +} +.cms-info-close:hover { + color: var(--color-text-readable, var(--color-text)); + border-color: color-mix(in srgb, var(--color-primary) 40%, transparent); +} +.cms-info-kicker { + color: var(--color-primary-readable, var(--color-primary)); + font-size: 0.74rem; + font-weight: 900; + letter-spacing: 0; + text-transform: uppercase; +} +.cms-info-dialog h2 { + margin: 0.2rem 2.2rem 0 0; + font-size: 1.75rem; + font-weight: 900; + line-height: 1.1; +} +.cms-info-badge { + display: inline-flex; + align-items: center; + margin-top: 0.65rem; + border: 1px solid color-mix(in srgb, var(--color-primary) 34%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--color-primary) 14%, transparent); + color: var(--color-text-readable, var(--color-text)); + font-size: 0.75rem; + font-weight: 900; + padding: 0.3rem 0.6rem; +} +.cms-info-copy { + margin: 0.9rem 0 0; + color: var(--color-text-muted-readable, var(--color-text-muted)); + font-size: 0.92rem; + line-height: 1.55; +} +.cms-info-grid { + display: grid; + gap: 0.5rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin: 1rem 0 0; +} +.cms-info-grid div { + border: 1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--color-background) 68%, transparent); + padding: 0.7rem; +} +.cms-info-grid dt { + color: var(--color-text-muted-readable, var(--color-text-muted)); + font-size: 0.68rem; + font-weight: 800; + text-transform: uppercase; +} +.cms-info-grid dd { + margin: 0.18rem 0 0; + color: var(--color-text-readable, var(--color-text)); + font-size: 0.8rem; + font-weight: 900; + line-height: 1.25; +} +.cms-info-credit-list { + display: grid; + gap: 0.5rem; + margin-top: 1rem; +} +.cms-info-credit { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + border: 1px solid color-mix(in srgb, var(--color-primary) 18%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--color-primary) 8%, transparent); + padding: 0.65rem 0.75rem; +} +.cms-info-credit span { + color: var(--color-text-readable, var(--color-text)); + font-weight: 900; +} +.cms-info-credit small { + color: var(--color-text-muted-readable, var(--color-text-muted)); + font-size: 0.74rem; + font-weight: 700; + text-align: right; +} + +@media (max-width: 520px) { + .cms-info-dialog { padding: 1rem; } + .cms-info-grid { grid-template-columns: 1fr; } + .cms-info-credit { + align-items: flex-start; + flex-direction: column; + gap: 0.2rem; + } + .cms-info-credit small { text-align: left; } +} diff --git a/src/components/cms-info-popup.test.tsx b/src/components/cms-info-popup.test.tsx new file mode 100644 index 00000000..e8e01394 --- /dev/null +++ b/src/components/cms-info-popup.test.tsx @@ -0,0 +1,10 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { CmsInfoPopup } from "@/components/cms-info-popup"; + +describe("CmsInfoPopup", () => { + it("renders the footer trigger", () => { + const html = renderToStaticMarkup(); + expect(html).toContain("CMS Info"); + }); +}); diff --git a/src/components/cms-info-popup.tsx b/src/components/cms-info-popup.tsx new file mode 100644 index 00000000..d7a197e2 --- /dev/null +++ b/src/components/cms-info-popup.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect, useState } from "react"; + +const cmsDetails = [ + ["Edition", "Next.js rebuild"], + ["Interface", "Public CMS"], + ["Runtime", "Next.js App Router"], +]; + +const credits = [ + ["Remco", "AtomCMS direction and original project"], + ["Life", "Next.js experience and CMS refinements"], +]; + +export function CmsInfoPopup() { + const [open, setOpen] = useState(false); + + useEffect(() => { + if (window.location.hash === "#cms-info") setOpen(true); + }, []); + + useEffect(() => { + if (!open) return; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setOpen(false); + }; + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [open]); + + return ( + <> + + {open ? ( +
setOpen(false)}> +
event.stopPropagation()}> + +
CMS information
+

AtomCMS

+
Next.js edition
+

A modern web layer for the hotel community, focused on fast navigation, clean administration, and a familiar AtomCMS experience.

+
+ {cmsDetails.map(([label, value]) =>
{label}
{value}
)} +
+
+ {credits.map(([name, role]) =>
{name}{role}
)} +
+
+
+ ) : null} + + ); +} diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx index 26328a02..228a724f 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { CmsInfoPopup } from "@/components/cms-info-popup"; import { siteSettings } from "@/lib/services/site-settings"; export async function SiteFooter() { @@ -8,7 +9,7 @@ export async function SiteFooter() { return ( ); }