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
6 changes: 2 additions & 4 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ NODE_ENV=development
NODE_ENV=development

# GitHub API - Access Token
GITHUB_USERNAME=hammy
GITHUB_REPOSITORY=hammy/hammayo.github.io
GITHUB_TOKEN=github_pat_11A...um9

# GitHub Repository
GITHUB_REPOSITORY=hammayo/www-portfolio

# Analytics
GA_MEASUREMENT_ID=G-8NQ.....1K

4 changes: 3 additions & 1 deletion src/app/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import { PageTransitionWrapper } from "@/components/page-transition-wrapper";
import { ContactCard } from "@/components/contact-card";
import { SOCIAL } from "@/lib/constants";
import type { Metadata } from "next";
import { PageViewEvent } from "@/components/analytics-event";

export const metadata: Metadata = {
title: "Contact | Hammayo's Portfolio",
description: "Get in touch with me through various channels.",
description: "Get in touch with me",
};

export default function ContactPage() {
return (
<PageTransitionWrapper>
<PageViewEvent page="contact" />
<Container>
<PageHeading
title="Contact"
Expand Down
2 changes: 0 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Header } from "@/components/header";
import { AnimatedBackground } from "@/components/animated-background";
import { basePath } from "@/lib/env";
import { SITE, THEME } from "@/lib/constants";
import { Analytics } from "@/components/analytics";
import { RouteProgress } from "@/components/route-progress";
import { Footer } from "@/components/footer";
import { ErrorBoundary } from "@/components/error-boundary";
Expand Down Expand Up @@ -88,7 +87,6 @@ export default function RootLayout({
</div>
</ErrorBoundary>
</ThemeProvider>
<Analytics />
</body>
</html>
);
Expand Down
2 changes: 2 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Container } from "@/components/container";
import { HeroTitle } from "@/components/hero-title";
import { PageTransitionWrapper } from "@/components/page-transition-wrapper";
import { PageViewEvent } from "@/components/analytics-event";

function generateStardate(): string {
const now = new Date();
Expand All @@ -13,6 +14,7 @@ function generateStardate(): string {
export default function HomePage() {
return (
<PageTransitionWrapper>
<PageViewEvent page="home" />
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-144px)]">
<Container className="text-center">
<div className="my-8 opacity-0 animate-fade-in animate-delay-500">
Expand Down
8 changes: 5 additions & 3 deletions src/app/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Container } from "@/components/container";
import { PageHeading } from "@/components/page-heading";
import { PageTransitionWrapper } from "@/components/page-transition-wrapper";
import { ProjectCard } from "@/components/project-card";
import type { Metadata } from "next";
import { fetchGitHubData } from "@/lib/github";
import type { Metadata } from "next";
import { PageViewEvent } from "@/components/analytics-event";

export const metadata: Metadata = {
title: "Projects | Hammayo's Portfolio",
Expand All @@ -16,7 +17,7 @@ export const revalidate = 3600;
export default async function ProjectsPage() {
const { pinnedRepos, otherRepos } = await fetchGitHubData({
username: 'hammayo',
maxRepos: 10,
maxRepos: 5,
includeForked: true
});

Expand All @@ -31,10 +32,11 @@ export default async function ProjectsPage() {

return (
<PageTransitionWrapper>
<PageViewEvent page="projects" />
<Container>
<PageHeading
title="Projects"
description="Explore my latest projects and open source contributions on GitHub."
description="Explore my most recent projects and open source contributions."
/>

{pinnedRepos.length === 0 && otherRepos.length === 0 ? (
Expand Down
20 changes: 20 additions & 0 deletions src/components/analytics-event.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import { useEffect } from 'react';
import { sendAnalyticsEvent } from './analytics';

type PageViewEventProps = {
page: string;
};

export function PageViewEvent({ page }: PageViewEventProps) {
useEffect(() => {
sendAnalyticsEvent({
action: 'page_view',
category: 'navigation',
label: page,
});
}, [page]);

return null;
}
80 changes: 15 additions & 65 deletions src/components/analytics.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'use client';

import { env } from '@/lib/env';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, Suspense } from 'react';
import { logger } from '@/lib/logger';

// Analytics event types
Expand All @@ -13,36 +11,11 @@ type AnalyticsEvent = {
value?: number;
};

// Send a page view to Google Analytics
const sendPageView = (path: string, search: string) => {
const url = path + search;

if (typeof window.gtag !== 'undefined') {
window.gtag('config', env.GA_MEASUREMENT_ID || '', {
page_path: url,
});
logger.debug(`📊 Page view sent: ${url}`);
}
};

// Send a custom event to Google Analytics
export const sendAnalyticsEvent = ({ action, category, label, value }: AnalyticsEvent) => {
if (typeof window.gtag !== 'undefined') {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
logger.debug(`📊 Event sent: ${action} - ${category}${label ? ` - ${label}` : ''}`);
}
};

// Initialize Google Analytics
function initializeGA() {
if (typeof window !== 'undefined' && env.GA_MEASUREMENT_ID) {
const gaId = env.GA_MEASUREMENT_ID;
if (!gaId || typeof window === 'undefined') return;

// Create script elements manually to avoid Next.js preloading

// Create script elements
const gtagScript = document.createElement('script');
gtagScript.async = true;
gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`;
Expand All @@ -55,6 +28,7 @@ function initializeGA() {
gtag('config', '${gaId}', {
page_path: window.location.pathname,
transport_type: 'beacon',
send_page_view: true
});
`;

Expand All @@ -65,39 +39,15 @@ function initializeGA() {
logger.debug(`📊 Google Analytics initialized for ${gaId}`);
}

// Page view tracker component that uses the useSearchParams hook
function PageViewTracker() {
const pathname = usePathname();
const searchParams = useSearchParams();

// Initialize GA on mount
useEffect(() => {
initializeGA();
}, []);

// Track page views
useEffect(() => {
if (env.GA_MEASUREMENT_ID && searchParams instanceof URLSearchParams) {
const searchString = searchParams.toString();
const search = searchString.length > 0 ? `?${searchString}` : '';
if (pathname) {
sendPageView(pathname, search);
}
}
}, [pathname, searchParams]);

return null;
}

// Main Analytics component
export function Analytics() {
if (!env.GA_MEASUREMENT_ID) {
return null;
// Send a custom event to Google Analytics
export const sendAnalyticsEvent = ({ action, category, label, value }: AnalyticsEvent) => {
if (typeof window !== 'undefined' && typeof window.gtag !== 'undefined' && env.GA_MEASUREMENT_ID) {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
send_to: env.GA_MEASUREMENT_ID,
});
logger.debug(`📊 Event sent: ${action} - ${category}${label ? ` - ${label}` : ''}`);
}

return (
<Suspense fallback={null}>
<PageViewTracker />
</Suspense>
);
}
};
2 changes: 1 addition & 1 deletion src/components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ export function Footer() {
</div>
</div>
</div>
</footer>
</footer>
);
}
49 changes: 49 additions & 0 deletions src/components/loading-spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client"

import { motion } from "framer-motion";
//import { motion } from "motion/react"

function LoadingCircleSpinner() {
return (
<div className="container">
<motion.div
className="spinner"
animate={{ rotate: 360 }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "linear",
}}
/>
<StyleSheet />
</div>
)
}

// Styles
function StyleSheet() {
return (
<style>
{`
.container {
display: flex;
justify-content: center;
align-items: center;
padding: 40px;
border-radius: 8px;
}

.spinner {
width: 50px;
height: 50px;
border-radius: 50%;
border: 4px solid var(--divider);
border-top-color: #ff0088;
will-change: transform;
}
`}
</style>
)
}

export default LoadingCircleSpinner