Add GDPR compliant cookie banner and update footer/privacy policy to include GA and Clarity; added e2e and unit tests for cookie handling; updated snapshots
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
--font-family-heading: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif;
|
||||
|
||||
--font-size-base: 18px;
|
||||
--font-size-xs: 14px;
|
||||
--font-size-small: 15px;
|
||||
--font-size-medium: 16px;
|
||||
--font-size-large: 20px;
|
||||
@@ -382,6 +383,14 @@ a {
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.small {
|
||||
border-radius: var(--border-radius-small);
|
||||
font-size: var(--font-size-xs);
|
||||
min-height: 36px;
|
||||
min-width: fit-content;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
|
||||
@@ -53,6 +53,13 @@
|
||||
Terms of Service
|
||||
</a>
|
||||
</nav>
|
||||
<p class="legal-notice">
|
||||
We improve our products and advertising by using Google Analytics and
|
||||
Microsoft Clarity to see how you use our website. By using our site, you agree
|
||||
that we and Microsoft can collect and use this data. Our <a
|
||||
href="/privacy-policy">privacy policy</a
|
||||
> has more details.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -82,4 +89,22 @@
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.legal-notice {
|
||||
margin-top: var(--space-md);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--color-text-tertiary);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.legal-notice a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
.legal-notice a:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
<span
|
||||
class={['mobile nav-header-logo', { 'page-home': bodyClass === 'page-home' }]}
|
||||
>
|
||||
<Wordmark />
|
||||
<a href="/" class="logo-link">
|
||||
<Wordmark />
|
||||
<span class="sr-only">mifi Ventures home page</span>
|
||||
</a>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -64,7 +67,7 @@
|
||||
]}
|
||||
>
|
||||
{#if page !== 'home'}
|
||||
<a href="/">
|
||||
<a href="/" class="logo-link">
|
||||
<Wordmark />
|
||||
<span class="sr-only">mifi Ventures home page</span>
|
||||
</a>
|
||||
@@ -155,6 +158,15 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
& .logo-link {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Hamburger toggle: mobile only, animates to X when open */
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
/**
|
||||
* Privacy Policy content for mifi Ventures. Used by /privacy-policy.
|
||||
* Last updated: March 5, 2026. Includes Messaging Policy (SMS) for OpenPhone / A2P compliance.
|
||||
* Last updated: March 12, 2026. Includes Messaging Policy (SMS) for OpenPhone / A2P compliance; Microsoft Clarity and Google Analytics.
|
||||
*/
|
||||
|
||||
export interface LegalSectionLink {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface LegalSection {
|
||||
id: string;
|
||||
heading: string;
|
||||
body: string[];
|
||||
list?: string[];
|
||||
/** Optional links to inject into body paragraphs (URLs in text become <a> with this label) */
|
||||
links?: LegalSectionLink[];
|
||||
/** Numbered sub-sections (e.g. Messaging Policy 1–6) */
|
||||
subsections?: { title: string; body: string[]; list?: string[] }[];
|
||||
}
|
||||
|
||||
export const privacyPolicy = {
|
||||
title: 'Privacy Policy',
|
||||
lastUpdated: 'March 5, 2026',
|
||||
lastUpdated: 'March 12, 2026',
|
||||
intro: [
|
||||
'mifi Ventures LLC respects your privacy and is committed to protecting personal information shared through this website and related communications.',
|
||||
'This policy explains what information we collect, how it is used, and how it is protected.',
|
||||
@@ -56,6 +63,31 @@ export const privacyPolicy = {
|
||||
'This information is used only to maintain the website, improve performance, and monitor security.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'analytics-and-tracking',
|
||||
heading: 'Analytics and Tracking Technologies',
|
||||
body: [
|
||||
'We use third-party analytics and advertising tools to understand how visitors use this website and to improve our services.',
|
||||
'We partner with Microsoft Clarity and Microsoft Advertising to capture how you use and interact with our website through behavioral metrics, heatmaps, and session replay to improve and market our products and services. Website usage data is captured using first- and third-party cookies and similar tracking technologies to determine the popularity of content and online activity. We also use this information for site optimization, security and fraud detection, and advertising.',
|
||||
'For more information about how Microsoft collects and uses your data, see the Microsoft Privacy Statement: https://www.microsoft.com/privacy/privacystatement.',
|
||||
'We also use Google Analytics to collect information about website usage, such as pages visited, time on site, and browser and device information. Google Analytics uses cookies and similar technologies to help us analyze how visitors use the site and to compile aggregated statistics.',
|
||||
'You can learn more about how Google handles data in Google Analytics at: https://policies.google.com/privacy and https://policies.google.com/technologies/partner-sites.',
|
||||
],
|
||||
links: [
|
||||
{
|
||||
href: 'https://www.microsoft.com/privacy/privacystatement',
|
||||
label: 'Microsoft Privacy Statement',
|
||||
},
|
||||
{
|
||||
href: 'https://policies.google.com/privacy',
|
||||
label: 'Google Privacy Policy',
|
||||
},
|
||||
{
|
||||
href: 'https://policies.google.com/technologies/partner-sites',
|
||||
label: 'How Google uses data from sites and apps',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'how-we-use',
|
||||
heading: 'How We Use Information',
|
||||
|
||||
@@ -29,17 +29,12 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-36F29PDKRT"></script>
|
||||
<script defer src="/assets/js/ga-init.js"></script>
|
||||
<script
|
||||
defer
|
||||
src="https://analytics.mifi.holdings/script.js"
|
||||
data-website-id="72ac01ce-e7fc-4582-8593-703f15add8d5"
|
||||
></script>
|
||||
<script defer src="/assets/js/umami-helper.js"></script>
|
||||
<!-- Microsoft Clarity -->
|
||||
<script defer src="https://www.clarity.ms/tag/vuo5q3yf79?ref=bwt"></script>
|
||||
|
||||
<title>{merged.title}</title>
|
||||
<meta name="description" content={merged.description ?? ''} />
|
||||
@@ -151,6 +146,7 @@
|
||||
|
||||
<script src="/assets/js/copyright-year.js" defer></script>
|
||||
<script src="/assets/js/mobile-menu-helper.js" defer></script>
|
||||
<script src="/assets/js/cookie-consent.js" defer></script>
|
||||
</svelte:head>
|
||||
|
||||
<a href="#main" class="skip-link" data-umami-event="skip to main content"
|
||||
@@ -158,6 +154,24 @@
|
||||
>
|
||||
{@render children()}
|
||||
<Footer />
|
||||
<div id="cookie-banner" class="cookie-banner" role="region" aria-label="Cookie consent">
|
||||
<div class="cookie-banner-content">
|
||||
<p class="cookie-notification-text">
|
||||
We use first-party analytics and, if you accept, third-party tools (e.g.
|
||||
Google, Microsoft) to understand usage and improve this site. You can accept
|
||||
all or reject non-essential analytics.
|
||||
<a href="/privacy-policy#analytics-and-tracking">Learn more</a>.
|
||||
</p>
|
||||
<div class="cookie-banner-actions">
|
||||
<button type="button" class="btn btn-primary small" data-consent="accept"
|
||||
>Accept all</button
|
||||
>
|
||||
<button type="button" class="btn btn-secondary small" data-consent="reject"
|
||||
>Reject non-essential</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="https://analytics.mifi.holdings/p/wQ9GYnLIg"
|
||||
alt=""
|
||||
@@ -166,3 +180,105 @@
|
||||
role="presentation"
|
||||
loading="eager"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
background-color: var(--color-bg-elevated, var(--color-bg));
|
||||
color: var(--color-text);
|
||||
border-top: 1px solid var(--color-border);
|
||||
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.16);
|
||||
|
||||
&:global(.is-visible) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.cookie-banner-content {
|
||||
container: cookie-banner / inline-size;
|
||||
max-width: var(--max-narrow-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--space-md) var(--space-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.cookie-notification-text {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-base);
|
||||
|
||||
@container cookie-banner (width >= 644px) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@container cookie-banner (width < 644px) {
|
||||
flex: 0 0 100%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.16em;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cookie-banner-actions {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
@container cookie-banner (width >= 644px) {
|
||||
flex: 0 0 auto;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
@container cookie-banner (width < 644px) {
|
||||
flex: 0 0 100%;
|
||||
gap: var(--space-sm);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
& .btn {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& [data-consent='accept'] {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
& [data-consent='reject'] {
|
||||
background-color: transparent;
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surface-subtle, rgba(148, 163, 184, 0.16));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import { privacyPolicy } from '$lib/data/privacy-policy';
|
||||
import type { LegalSectionLink } from '$lib/data/privacy-policy';
|
||||
|
||||
const navItems = [{ label: 'Home', href: '/', umamiEventLabel: 'home' }];
|
||||
|
||||
type BodySegment = string | { type: 'link'; href: string; label: string };
|
||||
|
||||
/** Splits a paragraph by link URLs and returns text/link segments for rendering. */
|
||||
function linkify(para: string, links: LegalSectionLink[]): BodySegment[] {
|
||||
const matches: { index: number; link: LegalSectionLink }[] = [];
|
||||
for (const link of links) {
|
||||
const idx = para.indexOf(link.href);
|
||||
if (idx !== -1) matches.push({ index: idx, link });
|
||||
}
|
||||
matches.sort((a, b) => a.index - b.index);
|
||||
const result: BodySegment[] = [];
|
||||
let last = 0;
|
||||
for (const { index, link } of matches) {
|
||||
if (index > last) result.push(para.slice(last, index));
|
||||
result.push({ type: 'link', href: link.href, label: link.label });
|
||||
last = index + link.href.length;
|
||||
}
|
||||
if (last < para.length) result.push(para.slice(last));
|
||||
return result.length ? result : [para];
|
||||
}
|
||||
</script>
|
||||
|
||||
<Navigation items={navItems} page="privacy-policy" />
|
||||
@@ -36,6 +58,16 @@
|
||||
<a href="https://mifi.ventures" rel="noopener noreferrer"
|
||||
>https://mifi.ventures</a
|
||||
>
|
||||
{:else if section.links}
|
||||
{#each linkify(para, section.links) as segment}
|
||||
{#if typeof segment === 'string'}
|
||||
{segment}
|
||||
{:else}
|
||||
<a href={segment.href} rel="noopener noreferrer"
|
||||
>{segment.label}</a
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{para}
|
||||
{/if}
|
||||
@@ -116,6 +148,16 @@
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
}
|
||||
|
||||
.legal-content a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
.legal-content a:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.legal-section {
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user