diff --git a/_includes/layouts/base.njk b/_includes/layouts/base.njk index edddc52..891f424 100644 --- a/_includes/layouts/base.njk +++ b/_includes/layouts/base.njk @@ -154,6 +154,18 @@ Interactions + + + + + Dashboard + @@ -220,6 +232,13 @@ Interactions Search + + Dashboard + {# Mobile theme toggle #} Theme @@ -334,5 +353,75 @@ {# Client-side webmention fetcher - supplements build-time cache with real-time data #} + {# Admin auth detection - shows dashboard link + FAB when logged in #} + + + {# Floating Action Button - visible only when logged in #} +
+ {# Backdrop #} +
+ {# Menu items #} +
+ + + + + Page + + + + + + Bookmark + + + + + + Photo + + + + + + Article + + + + + + Note + +
+ {# FAB button #} + +
diff --git a/css/tailwind.css b/css/tailwind.css index 2e08d76..546d26b 100644 --- a/css/tailwind.css +++ b/css/tailwind.css @@ -370,6 +370,37 @@ } } +/* Admin UI - FAB and dashboard link */ +@layer components { + .fab-container { + @apply fixed bottom-6 right-6 z-50 flex flex-col items-end; + } + + .fab-backdrop { + @apply fixed inset-0 bg-black/20 dark:bg-black/40 z-40; + } + + .fab-button { + @apply relative z-50 w-14 h-14 rounded-full bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-600 text-white shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center; + } + + .fab-button:focus-visible { + @apply outline-2 outline-offset-2 outline-primary-500; + } + + .fab-menu { + @apply relative z-50 mb-3 flex flex-col gap-2 items-end; + } + + .fab-menu-item { + @apply flex items-center gap-3 px-4 py-3 rounded-xl bg-white dark:bg-surface-800 shadow-md hover:shadow-lg border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-200 hover:text-primary-600 dark:hover:text-primary-400 no-underline transition-all duration-150 text-sm font-medium; + } + + .admin-nav-link { + @apply text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 no-underline transition-colors py-2 inline-flex items-center gap-1; + } +} + /* Performance: content-visibility for off-screen rendering optimization */ @layer utilities { .content-auto { diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..632c9d0 --- /dev/null +++ b/js/admin.js @@ -0,0 +1,67 @@ +/** + * Admin UI auth detection + * Checks if the user is logged in to Indiekit by probing /session/login. + * Dispatches a custom event for Alpine.js components to react to. + */ + +(function () { + const cacheKey = 'indiekit-auth-status'; + const cacheTTL = 5 * 60 * 1000; // 5 minutes + + function getCachedStatus() { + try { + const cached = sessionStorage.getItem(cacheKey); + if (!cached) return null; + const parsed = JSON.parse(cached); + if (Date.now() - parsed.ts > cacheTTL) { + sessionStorage.removeItem(cacheKey); + return null; + } + return parsed.loggedIn; + } catch { + return null; + } + } + + function setCachedStatus(loggedIn) { + try { + sessionStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), loggedIn: loggedIn })); + } catch { + // sessionStorage full or unavailable + } + } + + function dispatch(loggedIn) { + document.dispatchEvent(new CustomEvent('indiekit:auth', { detail: { loggedIn: loggedIn } })); + if (loggedIn) { + document.body.setAttribute('data-indiekit-auth', 'true'); + } else { + document.body.removeAttribute('data-indiekit-auth'); + } + } + + function checkAuth() { + return fetch('/session/login', { credentials: 'same-origin', redirect: 'manual', cache: 'no-store' }) + .then(function (response) { + // opaqueredirect means 302 → user is logged in + return response.type === 'opaqueredirect'; + }) + .catch(function () { + return false; + }); + } + + // Cache-then-verify: show from cache instantly, correct in background + var cached = getCachedStatus(); + if (cached !== null) { + dispatch(cached); + } + + checkAuth().then(function (loggedIn) { + setCachedStatus(loggedIn); + // Only re-dispatch if different from cache or no cache existed + if (cached === null || cached !== loggedIn) { + dispatch(loggedIn); + } + }); +})();