Skip to content
Merged
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
40 changes: 25 additions & 15 deletions src/__tests__/identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,38 +304,33 @@ describe('identity copy', () => {
const cta = readFileSync('src/components/ContactCTA.astro', 'utf8');
expect(footer).toContain('class="colophon"');
expect(footer).toContain("What's New");
expect(footer).toContain('data-visit-stats');
expect(footer).toContain('hidden');
expect(footer).not.toContain('data-visit-stats');
expect(footer).not.toContain('showUpcoming');
expect(footer).toContain("What's New</a>");
// Space-middot-space as Astro text node so paint is What's New · N.
expect(footer).toContain("{' · '}");
expect(footer.slice(footer.indexOf('data-visit-stats'))).toContain("{' · '}");
// Fail the prior bug: newline/indent then middot (leading space collapsed in paint).
expect(footer).not.toMatch(/>\s*\n\s+·\s/);
expect(cta).toContain('LinkedIn · replies from me');
});

it('keeps Footer .visit-stats display:inline only when not [hidden]', () => {
const footer = readFileSync('src/components/Footer.astro', 'utf8');
expect(footer).toContain('.visit-stats:not([hidden])');
expect(footer).toContain('display: inline');
// Bare .visit-stats { display: inline } overrides UA [hidden]{display:none}.
expect(footer).not.toMatch(/\.visit-stats\s*\{\s*display:\s*inline/);
expect(footer).not.toContain('.visit-stats:not([hidden])');
expect(footer).not.toContain('.visit-stats');
expect(footer).not.toContain('data-visit-stats');
expect(footer).toContain("{' · '}");
expect(footer).toContain('https://www.linkedin.com/in/alehar/');
});

it('leads the colophon visit trigger with unique visitors and names PostHog', () => {
const footer = readFileSync('src/components/Footer.astro', 'utf8');
expect(footer).toContain('formatColophonVisits');
expect(footer).toContain('formatColophonVisitsTitle');
expect(footer).toContain("trigger.addEventListener('focus'");
expect(footer).toContain('shouldShowVisitCount(row.uniqueVisitors)');
expect(footer).not.toContain('formatPageviewCount(data.pageviews)');
expect(footer).not.toContain('white-space: nowrap');
expect(footer).toContain('.visit-stats:not([hidden])');
expect(footer).toContain("{' · '}");
expect(footer).not.toContain('formatColophonVisits');
expect(footer).not.toContain('formatColophonVisitsTitle');
expect(footer).not.toContain("trigger.addEventListener('focus'");
expect(footer).not.toContain('shouldShowVisitCount');
expect(footer).not.toContain('data-visit-count');
expect(footer).not.toContain('/api/visits');
expect(footer).toContain('https://www.linkedin.com/in/alehar/');
expect(footer).not.toContain('mailto:');
expect(footer).toContain('class="colophon"');
Expand Down Expand Up @@ -2816,4 +2811,19 @@ describe('identity copy', () => {
expect(contact).toContain('min-height: 44px');
expect(contact).toContain('min-width: 44px');
});

it('footer colophon has no visitor visit-count control (#1045)', () => {
const footer = readFileSync('src/components/Footer.astro', 'utf8');
expect(footer).toContain('class="colophon"');
expect(footer).toContain("What's New");
expect(footer).toContain('This site');
expect(footer).toContain("What's next");
expect(footer).toContain('Site success');
expect(footer).not.toContain('data-visit-stats');
expect(footer).not.toContain('data-visit-count');
expect(footer).not.toContain('visit-glance-panel');
expect(footer).not.toContain('/api/visits');
expect(footer).toContain('https://www.linkedin.com/in/alehar/');
expect(footer).not.toContain('mailto:');
});
});
199 changes: 1 addition & 198 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,7 @@ const currentYear = new Date().getFullYear();
<div class="colophon">
<a href="/whats-new/">What's New</a>{' · '}<a href="/this-site/">This site</a>
{' · '}<a href="/roadmap/">What's next</a>
{' · '}<a href="/okr/">Site success</a><span class="visit-stats" data-visit-stats hidden
>{' · '}<button
type="button"
class="visit-trigger"
data-visit-count
aria-expanded="false"
aria-controls="visit-glance-panel"
aria-haspopup="true"></button>
</span>
{' · '}<a href="/okr/">Site success</a>
</div>
</div>
<p class="socials">
Expand All @@ -44,134 +36,6 @@ const currentYear = new Date().getFullYear();
<a href="https://github.com/alehar9320" target="_blank" rel="noopener noreferrer"> GitHub</a>
</p>
</footer>
<div class="visit-panel" id="visit-glance-panel" hidden>
<span data-glance-all></span>
<span data-glance-7d></span>
<span data-glance-first></span>
</div>

<script>
import {
formatColophonVisits,
formatColophonVisitsTitle,
shouldShowVisitCount,
type VisitGlance,
} from '../utils/visit-stats';

const visitStats = document.querySelector('[data-visit-stats]');

function isVisitGlance(data: unknown): data is VisitGlance {
if (!data || typeof data !== 'object') return false;
const row = data as VisitGlance;
return (
shouldShowVisitCount(row.uniqueVisitors) &&
typeof row.pageviews === 'number' &&
Number.isFinite(row.pageviews) &&
typeof row.firstSeen === 'string' &&
Number.isFinite(row.pageviews7d) &&
Number.isFinite(row.uniqueVisitors7d)
);
}

if (visitStats instanceof HTMLElement) {
fetch('/api/visits')
.then(async (response) => {
if (response.status === 204 || !response.ok) return;

let data: unknown;
try {
data = await response.json();
} catch {
return;
}

if (!isVisitGlance(data)) return;

const trigger = visitStats.querySelector('[data-visit-count]');
const panel = document.getElementById('visit-glance-panel');
const all = panel?.querySelector('[data-glance-all]');
const last7d = panel?.querySelector('[data-glance-7d]');
const first = panel?.querySelector('[data-glance-first]');
if (
!(trigger instanceof HTMLButtonElement) ||
!(panel instanceof HTMLElement) ||
!(all instanceof HTMLElement) ||
!(last7d instanceof HTMLElement) ||
!(first instanceof HTMLElement)
) {
return;
}

const title = formatColophonVisitsTitle(data);
trigger.textContent = formatColophonVisits(data);
trigger.title = title;
trigger.setAttribute('aria-label', title);
all.textContent = title;
last7d.textContent = '';
first.textContent = '';
visitStats.hidden = false;
document.body.append(panel);

const place = () => {
const box = trigger.getBoundingClientRect();
panel.style.position = 'fixed';
panel.style.left = `${Math.max(8, Math.min(box.left, window.innerWidth - 280))}px`;
panel.style.top = 'auto';
panel.style.right = 'auto';
panel.style.bottom = `${Math.max(8, window.innerHeight - box.top + 8)}px`;
};

const setOpen = (open: boolean) => {
panel.hidden = !open;
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) place();
};

trigger.addEventListener('click', () => {
window.location.assign('/okr/');
});

trigger.addEventListener('focus', () => {
setOpen(true);
});
trigger.addEventListener('blur', (event) => {
const next = event.relatedTarget;
if (next instanceof Node && (trigger.contains(next) || panel.contains(next))) return;
setOpen(false);
});
trigger.addEventListener('pointerenter', (event) => {
if (event.pointerType !== 'mouse') return;
setOpen(true);
});
trigger.addEventListener('pointerleave', (event) => {
if (event.pointerType !== 'mouse') return;
if (event.relatedTarget instanceof Node && panel.contains(event.relatedTarget)) return;
setOpen(false);
});
panel.addEventListener('pointerleave', (event) => {
if (event.pointerType !== 'mouse') return;
setOpen(false);
});

document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') setOpen(false);
});
document.addEventListener('pointerup', (event) => {
if (panel.hidden) return;
const target = event.target;
if (!(target instanceof Node)) return;
if (trigger.contains(target) || panel.contains(target)) return;
setOpen(false);
});
window.addEventListener('resize', () => {
if (!panel.hidden) place();
});
})
.catch(() => {
// Fail-open: keep the count hidden.
});
}
</script>

<style>
footer {
Expand Down Expand Up @@ -249,67 +113,6 @@ const currentYear = new Date().getFullYear();
line-height: 1.5;
}

.visit-stats:not([hidden]) {
display: inline;
}

.visit-stats {
color: inherit;
font-size: inherit;
}

.visit-trigger {
display: inline-flex;
align-items: center;
min-height: 44px;
appearance: none;
-webkit-appearance: none;
margin: 0;
padding: 0;
border: 0;
background: none;
font: inherit;
color: var(--gray-400);
cursor: pointer;
touch-action: manipulation;
text-underline-offset: 0.25em;
text-decoration: 1px dotted underline transparent;
}

.visit-trigger:hover,
.visit-trigger:focus-visible {
text-decoration-color: currentColor;
}

.visit-trigger:focus-visible {
outline: 2px solid var(--accent-regular);
outline-offset: 4px;
border-radius: 0.25rem;
}

.visit-panel {
display: flex;
flex-direction: column;
gap: 0.25rem;
position: fixed;
z-index: 40;
min-width: 14rem;
max-width: 18rem;
padding: 0.5rem 0.75rem;
text-align: left;
font-size: 0.75rem;
line-height: 1.45;
color: var(--gray-200);
background: var(--gray-900);
border: 1px solid var(--gray-700);
border-radius: 0.375rem;
box-shadow: var(--shadow-sm);
}

.visit-panel[hidden] {
display: none !important;
}

@media (min-width: 50em) {
footer {
flex-direction: row;
Expand Down
Loading