Skip to content

Enhance blog, contact form, performance, and accessibility features#25

Merged
SlenderShield merged 6 commits intomainfrom
feature/improvement
Mar 24, 2026
Merged

Enhance blog, contact form, performance, and accessibility features#25
SlenderShield merged 6 commits intomainfrom
feature/improvement

Conversation

@SlenderShield
Copy link
Copy Markdown
Owner

Expand blog content and improve project filtering. Upgrade the contact form for Netlify submissions and optimize resource loading. Polish admin mutations for better user experience without full reloads. Enhance accessibility and responsive design. Implement Google Analytics 4 tracking for single-page application navigation.

Copilot AI review requested due to automatic review settings March 24, 2026 10:13
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 24, 2026

Deploy Preview for muralidharabhat ready!

Name Link
🔨 Latest commit 573c6ca
🔍 Latest deploy log https://app.netlify.com/projects/muralidharabhat/deploys/69c263d600e54300086472ac
😎 Deploy Preview https://deploy-preview-25--muralidharabhat.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 71
Accessibility: 100
Best Practices: 100
SEO: 100
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@SlenderShield SlenderShield merged commit 7486bbb into main Mar 24, 2026
7 checks passed
@SlenderShield SlenderShield deleted the feature/improvement branch March 24, 2026 10:14
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the portfolio site’s UX and content by adding GA4 SPA page tracking, improving admin CRUD flows to avoid full reloads, upgrading the contact form submission flow, and adding several performance/accessibility improvements (e.g., responsive images, modal resume preview, CSS focus/touch-target work).

Changes:

  • Add Google Analytics 4 initialization + SPA route-change pageview tracking.
  • Replace admin “reload after mutation” behavior with in-place refetch + status messaging.
  • Upgrade contact form submission (Netlify attempt + mailto fallback), add project stack filtering, add resume preview modal, and apply performance/accessibility polish (srcset/preconnect/CSS).

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/utils/analytics.ts New GA4 initialization + pageview/event helpers
src/hooks/usePageTracking.ts New hook to track SPA navigations
src/hooks/useInView.ts New IntersectionObserver hook for scroll/visibility patterns
src/hooks/useFocusTrap.ts New focus-trap hook intended for modal dialogs
src/hooks/useApi.ts Adds refetch to async resource hooks for post-mutation refresh
src/pages/admin/ManageProjects.tsx Removes full reloads; adds mutation status UI + refetch
src/pages/admin/ManagePosts.tsx Removes full reloads; adds mutation status UI + refetch
src/pages/ProjectsPage.tsx Adds “Tech Stack” chip filtering in addition to category/search
src/hooks/useContactForm.ts Adds Netlify submission attempt + status state + mailto fallback
src/pages/ContactPage.tsx Wires new contact form status/submitting props into UI
src/components/ContactBriefForm.tsx Updates props/UI to show submitting + success/error messages
src/components/ResumePreview.tsx Adds a modal resume preview flow
src/pages/AboutPage.tsx Adds “Preview Resume” entry point on About page
src/components/ContentRenderer.tsx Adds Unsplash responsive srcSet/sizes for images
src/main.tsx Calls GA initialization on app startup
src/app/App.tsx Installs SPA page tracking hook
src/index.css Adds modal styles + additional accessibility/responsive rules
src/content/blogPosts.json Adds new blog posts
public/sitemap.xml Adds new blog URLs to sitemap
public/_headers Adds caching + security headers configuration
netlify.toml Adds a forms-related config block + keeps SPA redirect
index.html Adds DNS-prefetch/preconnect hints for fonts/Unsplash
.env.example Documents expected environment variables (incl. GA measurement id)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +11 to +15

useEffect(() => {
// Track page view when route changes
trackPageView(location.pathname);
}, [location.pathname]);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usePageTracking only tracks location.pathname, so navigations that change search/hash (e.g. ?tag=..., in-page anchors) will be collapsed into the same GA path. Consider tracking the full route (pathname + search + hash) or at least including location.search to avoid under-reporting distinct SPA pages.

Suggested change
useEffect(() => {
// Track page view when route changes
trackPageView(location.pathname);
}, [location.pathname]);
const fullPath = `${location.pathname}${location.search}${location.hash}`
useEffect(() => {
// Track page view when route changes
trackPageView(fullPath);
}, [fullPath]);

Copilot uses AI. Check for mistakes.
body: params.toString(),
})

return response.ok || response.status === 404 // 404 means Netlify Forms not configured, fallback to mailto
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

submitViaNetlify treats HTTP 404 as success (return response.ok || response.status === 404). That will show a success message and (in the success branch) clear the form even though the submission likely wasn’t captured—effectively dropping the user’s message. If 404 is the “not configured” signal, this should return false so the mailto fallback runs, and the success path should only be for 2xx responses.

Suggested change
return response.ok || response.status === 404 // 404 means Netlify Forms not configured, fallback to mailto
return response.ok // non-2xx (including 404) will trigger mailto fallback

Copilot uses AI. Check for mistakes.
<p className="meta">
Your details are only used to pre-fill your email app. Nothing is
stored.
Your details are only used to send your brief. Nothing is stored.
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI copy says “Nothing is stored.”, but the updated hook attempts a Netlify Forms submission, which (when configured) stores the submission on Netlify. Please update this copy to reflect the actual behavior (e.g., that the message is sent to you and may be stored by the form provider), or gate the copy based on which submission path is used.

Suggested change
Your details are only used to send your brief. Nothing is stored.
Your details are only used to send your brief and to respond to your message, and may be stored and processed by our form provider.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +34
// Add responsive srcset for Unsplash images
let srcSet = ''
if (imageSrc.includes('unsplash.com')) {
srcSet = `${imageSrc}?w=400 400w, ${imageSrc}?w=800 800w, ${imageSrc}?w=1200 1200w`
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Unsplash srcSet builder appends ?w=... unconditionally. Many existing Unsplash URLs already include query parameters (e.g. ?auto=format&fit=crop...), so this will produce invalid URLs with multiple ? and drop existing params. Build the URL with new URL(imageSrc) and update/append the w param while preserving existing query string, then serialize back for srcSet.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +69
await refetch();
setTimeout(() => {
resetMutationStatus();
}, 2000);
} catch (err) {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as ManageProjects: setTimeout is used to reset UI state after mutations without any cleanup. If the admin navigates away quickly, the timer can fire after unmount. Track/clear the timeout in a cleanup to avoid leaking timers and state updates on unmounted components.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/analytics.ts
}
(window as unknown as Record<string, unknown>).gtag = gtag;
gtag('js', new Date());
gtag('config', measurementId, {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializeGA() calls gtag('config', measurementId, ...) which (in GA4) sends an automatic page_view by default. Since the app also sends page_view events via trackPageView() on route changes, the initial page load is likely double-counted. Consider setting send_page_view: false in the initial config and using a consistent approach for SPA pageviews (either repeated gtag('config', ...) calls on navigation or a single page_view event strategy).

Suggested change
gtag('config', measurementId, {
gtag('config', measurementId, {
// Disable automatic page_view; SPA pageviews are tracked via trackPageView()
send_page_view: false,

Copilot uses AI. Check for mistakes.
Comment thread src/hooks/useApi.ts
Comment on lines +10 to +22
const refetch = useCallback(async () => {
setLoading(true);
try {
const res = await loader();
setData(res);
setError(null);
} catch (err) {
const errorObj = err instanceof Error ? err : new Error('Unknown error');
setError(errorObj);
} finally {
setLoading(false);
}
}, [loader]);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refetch can update state after unmount (it doesn’t share the mounted guard used in the initial useEffect). If a component calls refetch() and then unmounts before it resolves, this can trigger React warnings and leaks. Consider adding an abort/mounted guard inside refetch (or using an AbortController passed down into loader).

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +73
await refetch();
setTimeout(() => {
resetMutationStatus();
}, 2000);
} catch (err) {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The success/reset flow uses setTimeout to update component state, but the timeout isn’t cleared if the user navigates away before it fires. This can cause state updates on an unmounted component. Store the timeout id in a ref and clear it in a useEffect cleanup (and/or clear any existing timeout before setting a new one).

Copilot uses AI. Check for mistakes.
Comment thread src/hooks/useFocusTrap.ts
Comment on lines +12 to +18
useEffect(() => {
// Store the element that had focus before modal opened
previousFocusRef.current = document.activeElement as HTMLElement;

const container = containerRef.current;
if (!container) return;

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useFocusTrap runs its effect only once (useEffect(..., [])) and returns early if containerRef.current is null on mount. In ResumePreview, the dialog element is conditionally rendered, so containerRef.current will be null on the initial mount and the focus trap will never activate when the modal opens. Consider adding an active parameter (e.g. useFocusTrap(isOpen)) and re-running the effect when active becomes true and containerRef.current is set.

Copilot uses AI. Check for mistakes.
</button>
</div>
<div className="modal-body">
<embed src={resumeUrl} type="application/pdf" />
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The <embed> used for the PDF preview doesn’t have an accessible name/description. For screen readers, consider adding a title attribute (or wrapping it with an element that provides a label) so the embedded document is announced meaningfully inside the dialog.

Suggested change
<embed src={resumeUrl} type="application/pdf" />
<embed
src={resumeUrl}
type="application/pdf"
title="Resume PDF preview"
aria-label="Resume PDF preview"
/>

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants