Enhance blog, contact form, performance, and accessibility features#25
Enhance blog, contact form, performance, and accessibility features#25SlenderShield merged 6 commits intomainfrom
Conversation
✅ Deploy Preview for muralidharabhat ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
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.
|
|
||
| useEffect(() => { | ||
| // Track page view when route changes | ||
| trackPageView(location.pathname); | ||
| }, [location.pathname]); |
There was a problem hiding this comment.
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.
| 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]); |
| body: params.toString(), | ||
| }) | ||
|
|
||
| return response.ok || response.status === 404 // 404 means Netlify Forms not configured, fallback to mailto |
There was a problem hiding this comment.
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.
| 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 |
| <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. |
There was a problem hiding this comment.
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.
| 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. |
| // 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` | ||
| } |
There was a problem hiding this comment.
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.
| await refetch(); | ||
| setTimeout(() => { | ||
| resetMutationStatus(); | ||
| }, 2000); | ||
| } catch (err) { |
There was a problem hiding this comment.
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.
| } | ||
| (window as unknown as Record<string, unknown>).gtag = gtag; | ||
| gtag('js', new Date()); | ||
| gtag('config', measurementId, { |
There was a problem hiding this comment.
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).
| gtag('config', measurementId, { | |
| gtag('config', measurementId, { | |
| // Disable automatic page_view; SPA pageviews are tracked via trackPageView() | |
| send_page_view: false, |
| 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]); |
There was a problem hiding this comment.
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).
| await refetch(); | ||
| setTimeout(() => { | ||
| resetMutationStatus(); | ||
| }, 2000); | ||
| } catch (err) { |
There was a problem hiding this comment.
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).
| useEffect(() => { | ||
| // Store the element that had focus before modal opened | ||
| previousFocusRef.current = document.activeElement as HTMLElement; | ||
|
|
||
| const container = containerRef.current; | ||
| if (!container) return; | ||
|
|
There was a problem hiding this comment.
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.
| </button> | ||
| </div> | ||
| <div className="modal-body"> | ||
| <embed src={resumeUrl} type="application/pdf" /> |
There was a problem hiding this comment.
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.
| <embed src={resumeUrl} type="application/pdf" /> | |
| <embed | |
| src={resumeUrl} | |
| type="application/pdf" | |
| title="Resume PDF preview" | |
| aria-label="Resume PDF preview" | |
| /> |

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.