diff --git a/django-base.html b/django-base.html new file mode 100644 index 0000000..0024e1a --- /dev/null +++ b/django-base.html @@ -0,0 +1,25 @@ +{% load static %} + + + + + + {% block title %}YourSite{% endblock %} + {% block extra_head %}{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + {% block extra_scripts %}{% endblock %} + + diff --git a/header.js b/header.js new file mode 100644 index 0000000..9cdff2b --- /dev/null +++ b/header.js @@ -0,0 +1,265 @@ +/* + Shared Header — hosted at https://quantumtaskai.com/header.js + Update nav links and colors in this file only. + All connected apps will reflect changes on next page load. + To bust cache after updates, add ?v=2 to the script src. +*/ + +(function () { + // ─── Guard: prevent double injection (e.g. React hot reload) ─── + if (document.getElementById('sh-header')) return; + + // ─── Config: update nav links here ────────────────────────────── + var NAV_LINKS = [ + { label: 'Home', url: 'https://quantumtaskai.com' }, + { label: 'AI Digital Branding', url: 'https://quantumtaskai.com/digital-branding.html' }, + { label: 'Blog', url: 'https://blog.quantumtaskai.com/' }, + ]; + + // ─── Styles ────────────────────────────────────────────────────── + var css = ` + :root { + --sh-bg: #0a0e1a; + --sh-accent: #3b82f6; + --sh-text: #ffffff; + } + + #sh-header { + position: sticky; + top: 0; + left: 0; + width: 100%; + height: 64px; + background: var(--sh-bg); + z-index: 99999; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + .sh-inner { + max-width: 1200px; + margin: 0 auto; + height: 100%; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .sh-logo { + font-size: 1.2rem; + font-weight: 700; + color: var(--sh-accent); + text-decoration: none; + letter-spacing: 0.02em; + white-space: nowrap; + } + + .sh-logo:hover { + color: var(--sh-accent); + opacity: 0.85; + } + + .sh-nav { + display: flex; + align-items: center; + gap: 8px; + list-style: none; + margin: 0; + padding: 0; + } + + .sh-nav a { + color: var(--sh-text); + text-decoration: none; + font-size: 0.92rem; + font-weight: 500; + padding: 6px 12px; + border-radius: 6px; + transition: background 0.18s, color 0.18s; + white-space: nowrap; + } + + .sh-nav a:hover { + background: rgba(59, 130, 246, 0.15); + color: var(--sh-accent); + } + + .sh-nav a.sh-active { + color: var(--sh-accent); + background: rgba(59, 130, 246, 0.12); + } + + /* ── Hamburger ── */ + .sh-burger { + display: none; + flex-direction: column; + justify-content: center; + gap: 5px; + width: 36px; + height: 36px; + cursor: pointer; + background: none; + border: none; + padding: 4px; + border-radius: 6px; + transition: background 0.18s; + } + + .sh-burger:hover { + background: rgba(255,255,255,0.08); + } + + .sh-burger span { + display: block; + width: 22px; + height: 2px; + background: var(--sh-text); + border-radius: 2px; + transition: transform 0.22s, opacity 0.22s; + } + + .sh-burger.sh-open span:nth-child(1) { + transform: translateY(7px) rotate(45deg); + } + .sh-burger.sh-open span:nth-child(2) { + opacity: 0; + } + .sh-burger.sh-open span:nth-child(3) { + transform: translateY(-7px) rotate(-45deg); + } + + /* ── Mobile menu ── */ + .sh-mobile-menu { + display: none; + position: absolute; + top: 64px; + left: 0; + width: 100%; + background: var(--sh-bg); + padding: 12px 0 20px; + border-top: 1px solid rgba(255,255,255,0.08); + box-shadow: 0 8px 24px rgba(0,0,0,0.4); + } + + .sh-mobile-menu.sh-open { + display: block; + } + + .sh-mobile-menu a { + display: block; + color: var(--sh-text); + text-decoration: none; + font-size: 0.98rem; + font-weight: 500; + padding: 12px 28px; + transition: background 0.15s, color 0.15s; + } + + .sh-mobile-menu a:hover { + background: rgba(59, 130, 246, 0.12); + color: var(--sh-accent); + } + + .sh-mobile-menu a.sh-active { + color: var(--sh-accent); + } + + @media (max-width: 768px) { + #sh-header { + height: 56px; + } + + .sh-nav { + display: none; + } + + .sh-burger { + display: flex; + } + + .sh-mobile-menu { + top: 56px; + } + } + `; + + // ─── Inject styles ─────────────────────────────────────────────── + var styleEl = document.createElement('style'); + styleEl.id = 'sh-styles'; + styleEl.textContent = css; + document.head.appendChild(styleEl); + + // ─── Detect active link ────────────────────────────────────────── + var currentUrl = window.location.href; + + function buildNavItems(isMobile) { + return NAV_LINKS.map(function (link) { + var isActive = currentUrl === link.url || + (link.url !== 'https://quantumtaskai.com' && currentUrl.indexOf(link.url) === 0); + var activeClass = isActive ? ' sh-active' : ''; + if (isMobile) { + return '' + link.label + ''; + } + return '
  • ' + link.label + '
  • '; + }).join(''); + } + + // ─── Build HTML ────────────────────────────────────────────────── + var headerHTML = ` +
    +
    + + + +
    + +
    + `; + + // ─── Inject header at top of body ─────────────────────────────── + document.body.insertAdjacentHTML('afterbegin', headerHTML); + + // ─── Hamburger logic ───────────────────────────────────────────── + var burger = document.getElementById('sh-burger'); + var mobileMenu = document.getElementById('sh-mobile-menu'); + + function openMenu() { + burger.classList.add('sh-open'); + mobileMenu.classList.add('sh-open'); + burger.setAttribute('aria-expanded', 'true'); + } + + function closeMenu() { + burger.classList.remove('sh-open'); + mobileMenu.classList.remove('sh-open'); + burger.setAttribute('aria-expanded', 'false'); + } + + burger.addEventListener('click', function (e) { + e.stopPropagation(); + mobileMenu.classList.contains('sh-open') ? closeMenu() : openMenu(); + }); + + // Close on outside click + document.addEventListener('click', function (e) { + if (!document.getElementById('sh-header').contains(e.target)) { + closeMenu(); + } + }); + + // Close on ESC + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeMenu(); + }); + + console.log('Shared header loaded ✓'); +})(); diff --git a/react-SharedHeader.jsx b/react-SharedHeader.jsx new file mode 100644 index 0000000..ea5eb14 --- /dev/null +++ b/react-SharedHeader.jsx @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; + +/** + * SharedHeader + * + * Dynamically loads the shared header script from your static site. + * Renders nothing itself — the script injects the header into . + * + * Usage: + * import SharedHeader from '@/components/SharedHeader'; + * // Place it once in your root layout: + * + * + * To bust cache after updating header.js, change the version param: + * const HEADER_URL = 'https://quantumtaskai.com/header.js?v=2'; + */ + +const HEADER_URL = 'https://quantumtaskai.com/header.js'; + +export default function SharedHeader() { + useEffect(() => { + // Avoid loading twice (e.g. StrictMode double-invoke) + if (document.getElementById('sh-header')) return; + if (document.querySelector(`script[src="${HEADER_URL}"]`)) return; + + const script = document.createElement('script'); + script.src = HEADER_URL; + script.async = true; + document.body.appendChild(script); + + return () => { + // Clean up on unmount + const injectedScript = document.querySelector(`script[src="${HEADER_URL}"]`); + if (injectedScript) injectedScript.remove(); + + const injectedHeader = document.getElementById('sh-header'); + if (injectedHeader) injectedHeader.remove(); + + const injectedStyles = document.getElementById('sh-styles'); + if (injectedStyles) injectedStyles.remove(); + }; + }, []); + + return null; +} diff --git a/react-layout-example.jsx b/react-layout-example.jsx new file mode 100644 index 0000000..2baf613 --- /dev/null +++ b/react-layout-example.jsx @@ -0,0 +1,25 @@ +import SharedHeader from '@/components/SharedHeader'; + +/** + * Root layout for Next.js (works for both app/ and pages/ router). + * + * app/ router: use this as app/layout.jsx + * pages/ router: use this as pages/_app.jsx, wrapping + * + * The padding-top on
    prevents page content from being hidden + * behind the sticky header (64px desktop / 56px mobile). + */ +export default function RootLayout({ children }) { + return ( + + + {/* Injects the shared header into via header.js */} + + +
    + {children} +
    + + + ); +} diff --git a/shared-header-README.md b/shared-header-README.md new file mode 100644 index 0000000..5d0441a --- /dev/null +++ b/shared-header-README.md @@ -0,0 +1,158 @@ +# Shared Header System + +One header file. Three apps. Zero duplication. + +## How It Works + +``` +header.js (hosted on your main static site) + │ + ├── Static HTML site → +``` + +See `static-site-example.html` for a full working example. + +--- + +## Step 3 — Django Setup + +In your `base.html` template, add after `{% load static %}`: + +```html + +``` + +All templates that extend `base.html` automatically get the header. +See `django-base.html` for a full base template example. + +--- + +## Step 4 — React / Next.js Setup + +**1. Copy the component** to your project: +``` +src/components/SharedHeader.jsx (from react-SharedHeader.jsx) +``` + +**2. Add it to your root layout** (from `react-layout-example.jsx`): +```jsx +import SharedHeader from '@/components/SharedHeader'; + +export default function RootLayout({ children }) { + return ( + + + +
    + {children} +
    + + + ); +} +``` + +--- + +## Updating Nav Links + +Edit the `NAV_LINKS` array at the top of `header.js`: + +```js +var NAV_LINKS = [ + { label: 'Home', url: 'https://yoursite.com' }, + { label: 'About', url: 'https://yoursite.com/about' }, + // add or remove items here +]; +``` + +All connected apps reflect the change on next page load. No deploys needed for Django or React — they load the script remotely. + +--- + +## Updating Colors + +Edit the CSS variables near the top of `header.js`: + +```css +:root { + --sh-bg: #0a0e1a; /* background */ + --sh-accent: #3b82f6; /* accent / active color */ + --sh-text: #ffffff; /* nav link text */ +} +``` + +--- + +## Adding a New App + +Any app — PHP, Ruby, Vue, plain HTML — needs exactly one line: + +```html + +``` + +--- + +## Cache Busting After Updates + +When you update `header.js`, browsers may serve the old cached version. +Append a version query param to force a fresh fetch: + +```html + + +``` + +Update this in all three places (static HTML, Django base template, `SharedHeader.jsx`). + +--- + +## Troubleshooting + +**Header not showing?** +- Open browser DevTools → Network tab → check if `header.js` loaded (200 status). +- Check the console for errors. + +**CORS error in Django or React?** +- Your static site's server must allow cross-origin requests for JS files. +- In nginx, add to your server block: + ```nginx + add_header Access-Control-Allow-Origin "*"; + ``` + +**Content flashes before header appears?** +- Add `min-height: 64px` to a placeholder div before the script tag, or load the script in `` with `defer`. + +**z-index conflict (header appears behind other elements)?** +- The header uses `z-index: 99999`. If something still overlaps it, check for `z-index` or `transform` on a parent element (transforms create new stacking contexts). + +**Double header on React hot reload?** +- `header.js` checks `if (document.getElementById('sh-header')) return;` — this prevents double injection automatically. diff --git a/static-site-example.html b/static-site-example.html new file mode 100644 index 0000000..327191b --- /dev/null +++ b/static-site-example.html @@ -0,0 +1,29 @@ + + + + + + Page Title - YourSite + + + + + + + +
    +

    Welcome to YourSite

    +

    The header above is injected automatically by header.js.

    +

    To use on any page, add one script tag:

    +
    +<script src="https://quantumtaskai.com/header.js"></script>
    +

    That's it. No extra HTML, no CSS imports, no dependencies.

    +
    + + +