Skip to content

Commit 61d3794

Browse files
Merge pull request #1 from abhinavkumar985:add-view-tracking
add view tracking
2 parents a346a75 + 699c395 commit 61d3794

10 files changed

Lines changed: 208 additions & 96 deletions

File tree

global.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// global.d.ts
2+
interface Window {
3+
dataLayer: any[];
4+
gtag: (...args: any[]) => void;
5+
}

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"devDependencies": {
2323
"@eslint/eslintrc": "^3",
2424
"@tailwindcss/postcss": "^4",
25+
"@types/gtag.js": "^0.0.20",
2526
"@types/jsdom": "^21.1.7",
2627
"@types/node": "^20",
2728
"@types/react": "^19",

src/app/about/page.tsx

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use client";
22
import { FaLinkedin, FaGithub, FaMedium } from 'react-icons/fa';
3-
import { motion } from "motion/react";
3+
import { motion, useInView } from "framer-motion"; // Import useInView
44
import Timeline from '../ui/components/Timeline';
5-
import { useEffect, useState } from 'react';
5+
import { useEffect, useState, useRef } from 'react'; // Import useRef
6+
import { trackContentView } from '../tracking';
67

78
export default function About() {
89
const [skills, setSkills] = useState<string[]>([]);
@@ -134,6 +135,46 @@ export default function About() {
134135
},
135136
];
136137

138+
// Ref for the "My Journey" section
139+
const journeySectionRef = useRef(null);
140+
const isJourneySectionInView = useInView(journeySectionRef, { once: true }); // Trigger only once
141+
142+
useEffect(() => {
143+
if (isJourneySectionInView) {
144+
trackContentView('journey_start');
145+
}
146+
}, [isJourneySectionInView]);
147+
148+
// Ref for the "Introduction" section
149+
const introSectionRef = useRef(null);
150+
const isIntroSectionInView = useInView(introSectionRef, { once: true });
151+
152+
useEffect(() => {
153+
if (isIntroSectionInView) {
154+
trackContentView('introduction');
155+
}
156+
}, [isIntroSectionInView]);
157+
158+
// Ref for the "Top Links" (Social Links) section
159+
const topLinksSectionRef = useRef(null);
160+
const isTopLinksSectionInView = useInView(topLinksSectionRef, { once: true });
161+
162+
useEffect(() => {
163+
if (isTopLinksSectionInView) {
164+
trackContentView('top_link');
165+
}
166+
}, [isTopLinksSectionInView]);
167+
168+
// Ref for the "Skills" section
169+
const skillsSectionRef = useRef(null);
170+
const isSkillsSectionInView = useInView(skillsSectionRef, { once: true });
171+
172+
useEffect(() => {
173+
if (isSkillsSectionInView) {
174+
trackContentView('skills');
175+
}
176+
}, [isSkillsSectionInView]);
177+
137178
return (
138179
<div className="min-h-screen" role="main" aria-labelledby="about-section">
139180
<section id="about" className="container mx-auto py-16 px-4">
@@ -142,6 +183,7 @@ export default function About() {
142183
initial={{ opacity: 0, y: 20 }}
143184
animate={{ opacity: 1, y: 0 }}
144185
transition={{ duration: 0.8 }}
186+
ref={introSectionRef} // Assign ref
145187
className="text-center"
146188
>
147189
<h1 id="about-section" className="text-5xl font-bold mb-4">
@@ -156,42 +198,51 @@ export default function About() {
156198
</motion.div>
157199

158200
{/* Social Links */}
159-
<div className="flex justify-center space-x-6 mt-8" aria-label="Social Media Links">
160-
<a
161-
href="https://www.linkedin.com/in/abhinavkumar985/"
162-
target="_blank"
163-
rel="noopener noreferrer"
164-
className="text-5xl hover:text-accent transition-colors"
165-
style={{ color: 'rgb(10, 102, 194)' }}
166-
aria-label="LinkedIn Profile"
167-
>
168-
<FaLinkedin />
169-
</a>
170-
<a
171-
href="https://github.com/abhinavkumar985"
172-
target="_blank"
173-
rel="noopener noreferrer"
174-
className="text-5xl hover:text-accent transition-colors"
175-
aria-label="GitHub Profile"
176-
>
177-
<FaGithub />
178-
</a>
179-
<a
180-
href="https://medium.com/@abhinavkumar985"
181-
target="_blank"
182-
rel="noopener noreferrer"
183-
className="text-5xl hover:text-accent transition-colors"
184-
aria-label="Medium Blog"
185-
>
186-
<FaMedium />
187-
</a>
188-
</div>
201+
<motion.div
202+
initial={{ opacity: 0, y: 20 }}
203+
animate={{ opacity: 1, y: 0 }}
204+
transition={{ duration: 0.8 }}
205+
ref={topLinksSectionRef} // Assign ref
206+
className="text-center"
207+
>
208+
<div className="flex justify-center space-x-6 mt-8" aria-label="Social Media Links">
209+
<a
210+
href="https://www.linkedin.com/in/abhinavkumar985/"
211+
target="_blank"
212+
rel="noopener noreferrer"
213+
className="text-5xl hover:text-accent transition-colors"
214+
style={{ color: 'rgb(10, 102, 194)' }}
215+
aria-label="LinkedIn Profile"
216+
>
217+
<FaLinkedin />
218+
</a>
219+
<a
220+
href="https://github.com/abhinavkumar985"
221+
target="_blank"
222+
rel="noopener noreferrer"
223+
className="text-5xl hover:text-accent transition-colors"
224+
aria-label="GitHub Profile"
225+
>
226+
<FaGithub />
227+
</a>
228+
<a
229+
href="https://medium.com/@abhinavkumar985"
230+
target="_blank"
231+
rel="noopener noreferrer"
232+
className="text-5xl hover:text-accent transition-colors"
233+
aria-label="Medium Blog"
234+
>
235+
<FaMedium />
236+
</a>
237+
</div>
238+
</motion.div>
189239

190240
{/* Timeline Section */}
191241
<motion.div
192242
initial={{ opacity: 0, y: 20 }}
193243
animate={{ opacity: 1, y: 0 }}
194244
transition={{ duration: 0.8, delay: 0.2 }}
245+
ref={journeySectionRef} // Assign the ref here
195246
className="mt-16"
196247
>
197248
<h2 className="text-4xl font-semibold text-center" id="timeline-section">
@@ -205,6 +256,7 @@ export default function About() {
205256
initial={{ opacity: 0, y: 20 }}
206257
animate={{ opacity: 1, y: 0 }}
207258
transition={{ duration: 0.8, delay: 0.4 }}
259+
ref={skillsSectionRef} // Assign ref
208260
className="mt-16"
209261
>
210262
<h2 className="text-3xl font-semibold text-center mb-8" id="skills-section">

src/app/layout.tsx

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Navigation from "./ui/components/Navigation";
55
import Footer from "./ui/components/Footer";
66
import { ThemeProvider, useTheme } from "./ui/components/ThemeContext";
77
import { useEffect } from "react";
8+
import { trackLink } from "./tracking";
9+
import AnalyticsProvider from "./ui/components/AnalyticsProvider";
810

911
const geistSans = Geist({
1012
variable: "--font-geist-sans",
@@ -24,29 +26,6 @@ export default function RootLayout({
2426
const gtmId = process.env.NEXT_PUBLIC_GTM_ID;
2527
const { theme } = useTheme();
2628

27-
useEffect(() => {
28-
// @ts-expect-error type error type error
29-
const handleOutboundLinkClick = (event) => {
30-
const link = event.target.closest('a');
31-
if (link && link.href && !link.href.includes(window.location.hostname)) {
32-
const d = new URL(link.href);
33-
// @ts-expect-error type error type error
34-
gtag('event', 'click', {
35-
event_category: 'outbound',
36-
event_label: link.href,
37-
event_value: link.textContent.trim() || d.hostname, // Add link text as event_value
38-
transport_type: 'beacon',
39-
});
40-
}
41-
};
42-
43-
document.addEventListener('click', handleOutboundLinkClick);
44-
45-
return () => {
46-
document.removeEventListener('click', handleOutboundLinkClick);
47-
};
48-
}, []);
49-
5029
return (
5130
<html lang="en" className={theme}>
5231
<head>
@@ -64,33 +43,19 @@ export default function RootLayout({
6443

6544
{/* Favicon */}
6645
<link rel="icon" href="/favicon.ico" />
67-
{gtmId && (
68-
<>
69-
<script
70-
async
71-
src={`https://www.googletagmanager.com/gtag/js?id=${gtmId}`}
72-
/>
73-
<script id="google-analytics">
74-
{`
75-
window.dataLayer = window.dataLayer || [];
76-
function gtag(){dataLayer.push(arguments);}
77-
gtag('js', new Date());
78-
gtag('config', '${gtmId}');
79-
`}
80-
</script>
81-
</>
82-
)}
8346
</head>
8447
<body
8548
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
8649
>
87-
<ThemeProvider>
88-
<Navigation />
89-
<main className="grid p-8 pb-20 sm:p-20 sm:mt-8 lg:mt-0">
90-
{children}
91-
</main>
92-
<Footer />
93-
</ThemeProvider>
50+
<AnalyticsProvider gtagId={gtmId || ''}>
51+
<ThemeProvider>
52+
<Navigation />
53+
<main className="grid p-8 pb-20 sm:p-20 sm:mt-8 lg:mt-0">
54+
{children}
55+
</main>
56+
<Footer />
57+
</ThemeProvider>
58+
</AnalyticsProvider>
9459
</body>
9560
</html>
9661
);

src/app/tracking.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
/**
3+
* Track link click
4+
* @param link
5+
*/
6+
7+
export const trackLink = (link: HTMLAnchorElement) => {
8+
if (!window.gtag) return;
9+
gtag('event', 'click', {
10+
event_category: 'outbound',
11+
event_label: link.href,
12+
event_value: link.text || link.getAttribute('aria-label') || link.getAttribute('title'), // Add link text as event_value
13+
transport_type: 'beacon',
14+
});
15+
}
16+
17+
export const trackContentView = (content: string) => {
18+
if (!window.gtag) return;
19+
gtag('event', 'view_content', {
20+
item_name: content.split(" ").join("_"), // Name of the important information
21+
});
22+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// providers/AnalyticsProvider.tsx
2+
'use client'; // This is a client component
3+
4+
import React, { useEffect } from 'react';
5+
import Script from 'next/script';
6+
import { trackLink } from '@/app/tracking';
7+
8+
interface AnalyticsProviderProps {
9+
gtagId: string;
10+
children: React.ReactNode;
11+
}
12+
13+
const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ gtagId, children }) => {
14+
useEffect(() => {
15+
window.dataLayer = window.dataLayer || [];
16+
function gtag() {
17+
window.dataLayer.push(arguments);
18+
}
19+
window.gtag = gtag;
20+
// @ts-expect-error
21+
gtag('js', new Date());
22+
// @ts-expect-error
23+
gtag('config', gtagId);
24+
25+
26+
27+
// @ts-expect-error type error type error
28+
const handleOutboundLinkClick = (event) => {
29+
const link = event.target.closest('a');
30+
if (link && link.href && !link.href.includes(window.location.hostname)) {
31+
trackLink(link);
32+
}
33+
};
34+
35+
document.addEventListener('click', handleOutboundLinkClick);
36+
return () => {
37+
document.removeEventListener('click', handleOutboundLinkClick);
38+
};
39+
}, [gtagId]);
40+
41+
return (
42+
<>
43+
<Script
44+
strategy="afterInteractive"
45+
src={`https://www.googletagmanager.com/gtag/js?id=${gtagId}`}
46+
/>
47+
{children}
48+
</>
49+
);
50+
};
51+
52+
export default AnalyticsProvider;

src/app/ui/components/Footer.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1+
"use client"; // Ensure this is a client component for hooks
2+
import { trackContentView } from '@/app/tracking';
3+
import { useEffect, useRef } from 'react';
14
import { FaLinkedin, FaGithub, FaMedium, FaDev, FaStackOverflow } from 'react-icons/fa';
5+
import { useInView } from 'framer-motion'; // Import useInView
26

37
const Footer = () => {
8+
const footerRef = useRef(null);
9+
const isInView = useInView(footerRef, { once: true }); // Trigger only once
10+
11+
useEffect(() => {
12+
if (isInView) {
13+
trackContentView('footer');
14+
}
15+
}, [isInView]);
16+
417
return (
5-
<footer className="bg-gray-900 dark:bg-gray-700 text-gray-100 dark:text-gray-300 mx-auto py-16 px-4 main-footer" role="contentinfo">
18+
<footer ref={footerRef} className="bg-gray-900 dark:bg-gray-700 text-gray-100 dark:text-gray-300 mx-auto py-16 px-4 main-footer" role="contentinfo">
619
<div className="container mx-auto text-center">
720
{/* Main Text */}
821
<h2 className="text-3xl font-bold text-teal-400 dark:text-teal-300 font-mono" aria-label="Living, learning, and leveling up">

0 commit comments

Comments
 (0)