Skip to content

Commit 28f241c

Browse files
committed
Add Google Tag Manager component for enhanced analytics tracking; integrate it into the layout for improved data collection and user insights.
1 parent d768109 commit 28f241c

File tree

9 files changed

+302
-2
lines changed

9 files changed

+302
-2
lines changed

app/components/ConsentPanel.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useState, useEffect } from 'react'
2+
3+
const EU_COUNTRIES = [
4+
'AT',
5+
'BE',
6+
'BG',
7+
'HR',
8+
'CY',
9+
'CZ',
10+
'DK',
11+
'EE',
12+
'FI',
13+
'FR',
14+
'DE',
15+
'GR',
16+
'HU',
17+
'IE',
18+
'IT',
19+
'LV',
20+
'LT',
21+
'LU',
22+
'MT',
23+
'NL',
24+
'PL',
25+
'PT',
26+
'RO',
27+
'SK',
28+
'SI',
29+
'ES',
30+
'SE',
31+
]
32+
33+
interface LocationResponse {
34+
country: string
35+
}
36+
37+
// Cookie helper functions
38+
function setCookie(name: string, value: string, days: number) {
39+
const date = new Date()
40+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
41+
const expires = `expires=${date.toUTCString()}`
42+
document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`
43+
}
44+
45+
function getCookie(name: string): string | null {
46+
const nameEQ = `${name}=`
47+
const ca = document.cookie.split(';')
48+
for (let i = 0; i < ca.length; i++) {
49+
let c = ca[i]
50+
while (c.charAt(0) === ' ') c = c.substring(1, c.length)
51+
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length)
52+
}
53+
return null
54+
}
55+
56+
export function ConsentPanel() {
57+
const [showConsent, setShowConsent] = useState(false)
58+
59+
useEffect(() => {
60+
// Check if user is from EU
61+
const checkLocation = async () => {
62+
try {
63+
// In development, always show the consent panel
64+
if (import.meta.env.DEV) {
65+
const consent = getCookie('tracking-consent')
66+
if (!consent) {
67+
setShowConsent(true)
68+
}
69+
return
70+
}
71+
72+
const response = await fetch('/api/location')
73+
const data = (await response.json()) as LocationResponse
74+
const isEU = EU_COUNTRIES.includes(data.country)
75+
const consent = getCookie('tracking-consent')
76+
77+
if (isEU && !consent) {
78+
setShowConsent(true)
79+
}
80+
} catch (error) {
81+
console.error('Error checking location:', error)
82+
}
83+
}
84+
85+
checkLocation()
86+
}, [])
87+
88+
const handleAccept = () => {
89+
// Store consent for 1 year
90+
setCookie('tracking-consent', 'accepted', 365)
91+
setShowConsent(false)
92+
}
93+
94+
const handleDecline = () => {
95+
// Store consent for 1 year
96+
setCookie('tracking-consent', 'declined', 365)
97+
setShowConsent(false)
98+
}
99+
100+
if (!showConsent) return null
101+
102+
return (
103+
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 p-4 shadow-lg z-50">
104+
<div className="container mx-auto max-w-4xl">
105+
<p className="text-sm mb-4">
106+
We use cookies and similar technologies to help personalize content and analyze traffic.
107+
By clicking "Accept", you consent to our use of cookies and tracking technologies.
108+
</p>
109+
<div className="flex gap-4">
110+
<button
111+
onClick={handleAccept}
112+
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
113+
>
114+
Accept
115+
</button>
116+
<button
117+
onClick={handleDecline}
118+
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600"
119+
>
120+
Decline
121+
</button>
122+
</div>
123+
</div>
124+
</div>
125+
)
126+
}

app/components/GoogleTagManager.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useEffect, useState } from 'react'
2+
3+
declare global {
4+
interface Window {
5+
dataLayer: any[]
6+
gtag: (...args: any[]) => void
7+
}
8+
}
9+
10+
const GTM_ID = 'G-FFMFRC59FL' as const
11+
12+
// Cookie helper function
13+
function getCookie(name: string): string | null {
14+
const nameEQ = `${name}=`
15+
const ca = document.cookie.split(';')
16+
for (let i = 0; i < ca.length; i++) {
17+
let c = ca[i]
18+
while (c.charAt(0) === ' ') c = c.substring(1, c.length)
19+
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length)
20+
}
21+
return null
22+
}
23+
24+
export function GoogleTagManager() {
25+
const [hasConsent, setHasConsent] = useState(false)
26+
27+
useEffect(() => {
28+
const consent = getCookie('tracking-consent')
29+
if (consent === 'accepted') {
30+
setHasConsent(true)
31+
}
32+
}, [])
33+
34+
useEffect(() => {
35+
if (!hasConsent) return
36+
37+
// Initialize dataLayer
38+
window.dataLayer = window.dataLayer || []
39+
function gtag(...args: any[]) {
40+
window.dataLayer.push(args)
41+
}
42+
window.gtag = gtag
43+
44+
// Load the GTM script
45+
const script = document.createElement('script')
46+
script.src = `https://www.googletagmanager.com/gtag/js?id=${GTM_ID}`
47+
script.async = true
48+
document.head.appendChild(script)
49+
50+
// Initialize GTM
51+
gtag('js', new Date())
52+
gtag('config', GTM_ID)
53+
54+
// Cleanup function
55+
return () => {
56+
document.head.removeChild(script)
57+
}
58+
}, [hasConsent])
59+
60+
return null
61+
}

app/root.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010

1111
import type { Route } from './+types/root'
1212
import { Navigation } from './components/Navigation'
13+
import { GoogleTagManager } from './components/GoogleTagManager'
14+
import { ConsentPanel } from './components/ConsentPanel'
1315
import './app.css'
1416

1517
export const links: Route.LinksFunction = () => []
@@ -92,6 +94,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
9294
<div className="pt-16">{children}</div>
9395
<ScrollRestoration />
9496
<Scripts />
97+
<GoogleTagManager />
98+
<ConsentPanel />
9599
</body>
96100
</html>
97101
)

app/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export default [
1010
'.well-known/appspecific/com.chrome.devtools.json',
1111
'routes/[.well-known].appspecific[.]com.chrome.devtools.json.tsx'
1212
),
13+
route('api/location', 'routes/api.location.ts'),
1314
] satisfies RouteConfig

app/routes/api.location.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
interface IpApiResponse {
2+
country_code: string
3+
[key: string]: any
4+
}
5+
6+
export async function loader() {
7+
try {
8+
const response = await fetch('https://ipapi.co/json/')
9+
const data = (await response.json()) as IpApiResponse
10+
11+
return new Response(JSON.stringify({ country: data.country_code }), {
12+
headers: { 'Content-Type': 'application/json' },
13+
})
14+
} catch (error) {
15+
console.error('Error fetching location:', error)
16+
return new Response(JSON.stringify({ country: 'US' }), {
17+
status: 500,
18+
headers: { 'Content-Type': 'application/json' },
19+
})
20+
}
21+
}

app/routes/home.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export function meta(_args: Route.MetaArgs) {
2727
}
2828

2929
export function loader({ context }: Route.LoaderArgs) {
30-
return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE }
30+
// Safely access the environment variable with a fallback
31+
const message = context.cloudflare?.env?.VALUE_FROM_CLOUDFLARE ?? 'Welcome'
32+
return { message }
3133
}
3234

3335
export default function Home() {

content/blog/2025-06-06-mothership-shopify-summit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ _**NOTE:** Spoilers for the [Mothership 1e](https://www.tuesdayknightgames.com/c
1919

2020
The following game took place during our annual Shopify meetup in Toronto on May 29, 2025. Eight crew members in nine brains were involved, and I hereby certify the accounts of this mission are unclassified and accurate to the best of my knowledge.
2121

22-
![SS Shopify Crew](/images/2025-06-06-mothership-shopify-summit.webp)
22+
![SS Shopify Crew](/images/2025-06-06-mothership-shopify-summit-small.webp)
2323

2424
## The Mission
2525

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"test:ci": "vitest run"
2020
},
2121
"dependencies": {
22+
"@remix-run/cloudflare": "^2.16.8",
2223
"@types/three": "^0.175.0",
2324
"class-variance-authority": "^0.7.1",
2425
"clsx": "^2.1.1",

0 commit comments

Comments
 (0)