Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 47 additions & 22 deletions apps/web/public/scripts/site.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Script from "next/script";
import type { ReactNode } from "react";
import { CrispBootstrap } from "@/components/CrispBootstrap";
import { JsonLd } from "@/components/JsonLd";
import { FirstVisitSplash } from "@/components/marketing/FirstVisitSplash";

/**
* Load Outfit Variable via Next.js Font API so the --font-outfit CSS variable
Expand Down Expand Up @@ -136,6 +137,11 @@ export default function RootLayout({ children }: { children: ReactNode }) {
/>
</noscript>
{children}
{/* Global first-visit splash. Mounted in the root layout so
it fires on the user's very first landing regardless of
entry URL. Reads `window.location` directly — no
`useSearchParams` / Suspense boundary required. */}
<FirstVisitSplash />
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
<CrispBootstrap />
{/* Stripe v3 — loaded eagerly so Wappalyzer/BuiltWith detect "Payment Processing" */}
<Script
Expand Down
294 changes: 201 additions & 93 deletions apps/web/src/components/marketing/FirstVisitSplash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,206 @@
import Link from "next/link";
import { useEffect, useState } from "react";

const STORAGE_KEY = "dba_first_visit_shown_v1";
/**
* Bumped from `_v1` -> `_v2` (splash moved out of HomePage into root layout)
* -> `_v3` (splash now reads `window.location` directly on mount instead of
* Next's `useSearchParams` hook, so there is no Suspense/hydration race —
* the effect fires on the user's very first paint regardless of entry URL).
* Old v1/v2 flags are ignored so every existing visitor is re-evaluated
* once under the new global rules.
*/
const STORAGE_KEY = "dba_first_visit_shown_v3";

const MICRO_SAAS_PATH_PREFIXES = ["/tools"] as const;
/**
* Routes that must NEVER show the splash regardless of entry conditions
* (e.g. PDF print views, embedded widgets). The print sub-route is rendered
* with no marketing chrome by design — the splash would otherwise overlay
* the printable output and end up in the PDF.
*/
const PRINT_PATH_PATTERN = /^\/lighthouse\/report\/[^/]+\/print\/?$/;
const MICRO_SAAS_PARAM_KEYS = [
"ref",
"source",
"utm_source",
"utm_campaign",
"utm_medium",
"utm_content",
] as const;
const MICRO_SAAS_PARAM_PATTERN = /micro[-_ ]?saas|microsaas/i;

function isMicroSaasEntry(pathname: string, search: string): boolean {
for (const prefix of MICRO_SAAS_PATH_PREFIXES) {
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
return true;
}
}
if (search) {
let params: URLSearchParams | null = null;
try {
params = new URLSearchParams(search);
} catch {
params = null;
}
if (params) {
for (const key of MICRO_SAAS_PARAM_KEYS) {
const value = params.get(key);
if (value && MICRO_SAAS_PARAM_PATTERN.test(value)) {
return true;
}
}
}
}
return false;
}

function safeGetStorage(key: string): string | null {
try {
return window.localStorage.getItem(key);
} catch {
return null;
}
}

function safeSetStorage(key: string, value: string): void {
try {
window.localStorage.setItem(key, value);
} catch {
// swallow — private-mode / storage-full / disabled cookies
}
}

export function FirstVisitSplash() {
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
// Check if already shown
const hasShown = localStorage.getItem(STORAGE_KEY);
if (!hasShown) {
// Show after short delay for better UX
const timer = setTimeout(() => setIsOpen(true), 1500);
return () => clearTimeout(timer);
}
}, []);

const handleClose = () => {
localStorage.setItem(STORAGE_KEY, "true");
setIsOpen(false);
};

const handleContactClick = () => {
localStorage.setItem(STORAGE_KEY, "true");
// Track the CTA
if (typeof window !== "undefined") {
const w = window as unknown as { dataLayer?: unknown[] };
w.dataLayer?.push({
event: "first_visit_splash_contact",
cta_source: "first_visit_splash",
});
}
setIsOpen(false);
};

if (!isOpen) return null;

return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
{/* Backdrop */}
<button
type="button"
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={handleClose}
aria-label="Close splash"
/>

{/* Modal — canonical glass surface (Phase 4 SEV-1 Linear/Stripe pivot).
4% → 1% white gradient, micro-border at white/8, frosted blur,
inset top-highlight is the new accent (replaces the heavy bronze line). */}
<div className="relative w-full max-w-lg overflow-hidden rounded-2xl border border-white/[0.08] bg-linear-to-b from-white/[0.04] to-white/[0.01] backdrop-blur-2xl p-8 md:p-10 shadow-[inset_0_1px_0_rgba(255,255,255,0.05),0_32px_80px_-32px_rgba(0,0,0,0.6)]">
{/* Sleek Close Button */}
<button
type="button"
onClick={handleClose}
className="splash-close-btn"
aria-label="Close"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 6L6 18M6 6l12 12"></path>
</svg>
</button>

<div className="mb-4">
<p className="mb-2 text-[0.65rem] font-bold uppercase tracking-[0.2em] text-[rgb(var(--accent-bronze-rgb)/0.9)]">
New: Micro SaaS Division
</p>
<h2 className="font-[family-name:var(--font-display)] text-2xl md:text-3xl font-medium tracking-[-0.035em] leading-[1.1] text-white">
Bespoke web apps & automations.
</h2>
</div>

<p className="mb-8 text-[0.95rem] leading-relaxed text-white/75">
From custom CRMs to complex API integrations—if your business needs it to scale, we can build it.
</p>

<div className="flex flex-col gap-3 sm:flex-row">
<Link
href="/contact"
onClick={handleContactClick}
className="btn-premium-primary flex-1 sm:flex-none"
>
Contact us →
</Link>

<button
type="button"
onClick={handleClose}
className="flex-1 sm:flex-none rounded-xl border border-white/10 bg-white/5 px-6 py-2.5 text-[0.95rem] font-medium text-white/80 transition hover:bg-white/10"
>
Explore the site
</button>
</div>
</div>
</div>
);
}
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
// Only evaluate once per session (on initial mount). After the first
// run we persist a flag so subsequent navigations never re-trigger.
if (safeGetStorage(STORAGE_KEY)) {
return;
}

// Read the initial landing URL directly from window.location so the
// check does not depend on Next's `useSearchParams` (which forces a
// Suspense boundary and can defer mount to a later hydration step).
// This guarantees the splash is evaluated on the user's very first
// paint, regardless of which route they land on.
const pathname =
typeof window !== "undefined" ? window.location.pathname : "";
const search = typeof window !== "undefined" ? window.location.search : "";

// Print/PDF sub-route is intentionally bare (no marketing chrome).
// Skip without persisting the flag so a normal subsequent visit
// to the marketing site still triggers the splash on first paint.
if (PRINT_PATH_PATTERN.test(pathname)) {
return;
}

// Micro-SaaS entry points (e.g. direct /tools landings, or any URL
// tagged with a micro-saas utm/ref param) must NEVER show the splash,
// since those visitors are already arriving for that exact offer.
if (isMicroSaasEntry(pathname, search)) {
safeSetStorage(STORAGE_KEY, "skipped-microsaas");
return;
}

const timer = window.setTimeout(() => setIsOpen(true), 1500);
return () => window.clearTimeout(timer);
}, []);

const markShown = () => {
safeSetStorage(STORAGE_KEY, "shown");
};

const handleClose = () => {
markShown();
setIsOpen(false);
};

const handleContactClick = () => {
markShown();
if (typeof window !== "undefined") {
const w = window as unknown as { dataLayer?: unknown[] };
w.dataLayer?.push({
event: "first_visit_splash_contact",
cta_source: "first_visit_splash",
});
}
setIsOpen(false);
};

if (!isOpen) return null;

return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
{/* Backdrop */}
<button
type="button"
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={handleClose}
aria-label="Close splash"
/>

{/* Modal */}
<div className="relative w-full max-w-2xl overflow-hidden rounded-2xl border border-[rgb(var(--accent-bronze-rgb)/0.3)] bg-linear-to-br from-[rgb(18_20_26/0.98)] to-[rgb(10_12_18/0.98)] p-12 md:p-14 shadow-2xl">
{/* Accent line */}
<div className="absolute left-0 right-0 top-0 h-[2px] bg-linear-to-r from-transparent via-[rgb(var(--accent-bronze-rgb)/0.8)] to-transparent" />

<button
type="button"
onClick={handleClose}
className="absolute right-6 top-6 rounded-full p-2 text-white/50 transition-colors hover:text-white"
aria-label="Close"
>
×
</button>

<div className="mb-8">
<p className="mb-3 text-xs font-bold uppercase tracking-[0.2em] text-[rgb(var(--accent-bronze-rgb)/0.9)]">
New: Micro SaaS Division
</p>
<h2 className="font-display text-3xl font-bold leading-tight text-white">
Custom web solutions for scaling businesses
</h2>
</div>

<p className="mb-8 text-base leading-relaxed text-white/75 max-w-lg">
Alongside our Mohawk Valley studio work, we now build bespoke web
applications and marketing automation systems for businesses ready to
scale. From custom CRMs to lead generation platforms — if you can
imagine it, we can build it.
</p>

<div className="mb-8 flex flex-wrap gap-3">
{["Custom Web Apps", "Automation", "API Integrations", "Scaling"].map(
(tag) => (
<span
key={tag}
className="rounded-full border border-white/10 bg-white/5 px-4 py-1.5 text-sm text-white/70"
>
{tag}
</span>
),
)}
</div>

<div className="flex flex-col gap-4 sm:flex-row">
<Link
href="/contact"
onClick={handleContactClick}
className="inline-flex items-center justify-center rounded-xl border border-[rgb(var(--accent-bronze-rgb)/0.6)] bg-linear-to-b from-[var(--accent-bronze-light)] to-[rgb(var(--accent-bronze-rgb))] px-8 py-3.5 text-base font-semibold text-[#171008] shadow-lg transition hover:-translate-y-px hover:shadow-xl"
>
Contact us →
</Link>
<button
type="button"
onClick={handleClose}
className="rounded-xl border border-white/10 bg-white/5 px-8 py-3.5 text-base font-medium text-white/80 transition hover:bg-white/10"
>
Explore the site
</button>
</div>
</div>
</div>
);
}
2 changes: 0 additions & 2 deletions apps/web/src/components/marketing/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
STANDARD_WEBSITE_STARTING_PRICE,
STANDARD_WEBSITE_TYPICAL_RANGE,
} from "@/lib/offers";
import { FirstVisitSplash } from "./FirstVisitSplash";
import { FoundingPartnerSection } from "./FoundingPartnerSection";
import { PremiumPitchStrip } from "./PremiumPitchStrip";
import "@/app/home-page.css";
Expand Down Expand Up @@ -506,7 +505,6 @@ export function HomePage() {
});
})();`}
</Script>
<FirstVisitSplash />
</>
);
}
20 changes: 17 additions & 3 deletions apps/web/src/components/marketing/MarketingChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,23 @@ export function MarketingChrome({
footerCta,
hidePreFooterCta,
minimalChrome,
headerCurrentSection,
footerBuildTag,
footerPoweredBy,
segmentClassName,
}: {
children: ReactNode;
footerCta?: FooterCtaProps;
hidePreFooterCta?: boolean;
minimalChrome?: boolean;
/** Marks the current section on the BrandHeader (e.g. "audit" on /lighthouse). */
headerCurrentSection?: "audit";
/** Optional mono build tag shown next to legal links in the BrandFooter. */
footerBuildTag?: string;
/** Optional "Powered by" stack shown in the BrandFooter. */
footerPoweredBy?: ReadonlyArray<{ label: string; href: string }>;
/** Optional class applied to the outer wrapper — used to scope segment-specific CSS (e.g. `lighthouse-segment`). */
segmentClassName?: string;
}) {
if (minimalChrome) {
return <>{children}</>;
Expand Down Expand Up @@ -112,7 +124,7 @@ window.__dbaRevokeAnalyticsConsent = function () {

<div id="reading-progress-bar" aria-hidden="true" />
<div className="site-chrome-sticky sticky top-0 z-[100] shadow-[0_12px_32px_rgba(3,7,18,0.34)]">
<BrandHeader />
<BrandHeader currentSection={headerCurrentSection} />
</div>

{/* Mobile nav overlay — `mobile-nav-overlay` + `open` are JS hooks (mobile-nav.ts). */}
Expand Down Expand Up @@ -174,12 +186,14 @@ window.__dbaRevokeAnalyticsConsent = function () {
</div>
</div>

<div className="site-body-canvas block min-w-0">
<div
className={`site-body-canvas block min-w-0${segmentClassName ? ` ${segmentClassName}` : ""}`}
>
<SiteContactDrawer />
<div className="min-w-0">
<main id="main-content">{children}</main>
{!hidePreFooterCta && footerCta ? <FooterCta {...footerCta} /> : null}
<BrandFooter />
<BrandFooter buildTag={footerBuildTag} poweredBy={footerPoweredBy} />
</div>
</div>

Expand Down
Loading