Skip to content

Commit 7486bbb

Browse files
Merge pull request #25 from SlenderShield:feature/improvement
Enhance blog, contact form, performance, and accessibility features
2 parents 6d72175 + 573c6ca commit 7486bbb

23 files changed

Lines changed: 1175 additions & 62 deletions

.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Environment Variables Configuration
2+
# Copy this file to .env.local for local development with environment variables
3+
4+
# Supabase Configuration (optional - falls back to static JSON)
5+
# Get these from your Supabase project settings
6+
VITE_SUPABASE_URL=https://your-project.supabase.co
7+
VITE_SUPABASE_ANON_KEY=your-anon-key
8+
9+
# Google Analytics Configuration (optional)
10+
# Get your Measurement ID from Google Analytics 4 property settings
11+
# Format: G-XXXXXXXXXX
12+
VITE_GA_MEASUREMENT_ID=
13+
14+
# Note: For production (Netlify), set environment variables in:
15+
# Netlify Site Settings → Build & Deploy → Environment → Environment Variables

index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77

8+
<!-- DNS prefetch and preconnect for critical resources -->
9+
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
10+
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
11+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12+
<link rel="dns-prefetch" href="https://images.unsplash.com" />
13+
<link rel="preconnect" href="https://images.unsplash.com" crossorigin />
14+
815
<!-- Primary SEO -->
916
<title>Muralidhara Bhat KS — Software Engineer · Distributed Systems & Backend</title>
1017
<meta name="description" content="Software engineer with 3.5+ years building high-throughput distributed systems, Java microservices, and event-driven architectures. Based in Bengaluru, India." />

netlify.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,17 @@
1010
from = "/*"
1111
to = "/index.html"
1212
status = 200
13+
14+
# Netlify Forms configuration
15+
[[forms]]
16+
name = "contact"
17+
method = "POST"
18+
fields = [
19+
{ name = "form-name", type = "hidden" },
20+
{ name = "name" },
21+
{ name = "email" },
22+
{ name = "topic" },
23+
{ name = "message" }
24+
]
25+
# Optional: configure where submissions go
26+
# notifications are sent to your Netlify email by default

public/_headers

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Cache-Control: public, max-age=3600
3+
X-Content-Type-Options: nosniff
4+
X-Frame-Options: DENY
5+
X-XSS-Protection: 1; mode=block
6+
Referrer-Policy: strict-origin-when-cross-origin
7+
Permissions-Policy: geolocation=(), microphone=(), camera=()
8+
9+
/index.html
10+
Cache-Control: public, max-age=300, must-revalidate
11+
X-Content-Type-Options: nosniff
12+
X-Frame-Options: DENY
13+
14+
/assets/*
15+
Cache-Control: public, max-age=86400, immutable, smax-age=86400
16+
17+
/Muralidhara_Bhat_Resume.pdf
18+
Cache-Control: public, max-age=86400
19+
Content-Type: application/pdf
20+
Content-Disposition: inline
21+
22+
/sitemap.xml
23+
Cache-Control: public, max-age=86400
24+
Content-Type: application/xml
25+
26+
/robots.txt
27+
Cache-Control: public, max-age=86400
28+
Content-Type: text/plain
29+
30+
/*.svg
31+
Cache-Control: public, max-age=86400, immutable
32+
Content-Type: image/svg+xml
33+
34+
/*.png
35+
Cache-Control: public, max-age=86400, immutable
36+
Content-Type: image/png

public/sitemap.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,22 @@
9797
<priority>0.7</priority>
9898
<lastmod>2026-03-14</lastmod>
9999
</url>
100+
<url>
101+
<loc>https://muralidharabhat.in/blog/motorcycles-and-code</loc>
102+
<changefreq>monthly</changefreq>
103+
<priority>0.7</priority>
104+
<lastmod>2026-02-20</lastmod>
105+
</url>
106+
<url>
107+
<loc>https://muralidharabhat.in/blog/bangalore-riding-guide</loc>
108+
<changefreq>monthly</changefreq>
109+
<priority>0.7</priority>
110+
<lastmod>2026-01-15</lastmod>
111+
</url>
112+
<url>
113+
<loc>https://muralidharabhat.in/blog/balancing-code-and-caffeine</loc>
114+
<changefreq>monthly</changefreq>
115+
<priority>0.7</priority>
116+
<lastmod>2025-12-10</lastmod>
117+
</url>
100118
</urlset>

src/app/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Suspense, lazy } from 'react'
22
import { Navigate, Route, Routes } from 'react-router-dom'
33
import { AuthProvider } from '../hooks/useAuth'
4+
import { usePageTracking } from '../hooks/usePageTracking'
45

56
const HomePage = lazy(() => import('../pages/HomePage').then((m) => ({ default: m.HomePage })))
67
const AboutPage = lazy(() => import('../pages/AboutPage').then((m) => ({ default: m.AboutPage })))
@@ -22,6 +23,9 @@ const ManagePosts = lazy(() => import('../pages/admin/ManagePosts').then((m) =>
2223
const ProtectedRoute = lazy(() => import('../components/ProtectedRoute').then((m) => ({ default: m.ProtectedRoute })))
2324

2425
export function App() {
26+
// Track page views on route changes
27+
usePageTracking()
28+
2529
return (
2630
<AuthProvider>
2731
<Suspense fallback={<div className="page-shell app-suspense-fallback" />}>

src/components/ContactBriefForm.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import type { FormEvent } from 'react';
2-
import type { ContactState } from '../hooks/useContactForm';
1+
import type { FormEvent } from 'react'
2+
import type { ContactState } from '../hooks/useContactForm'
33

44
type ContactBriefFormProps = {
5-
contact: ContactState;
6-
errors: Partial<ContactState>;
7-
sent: boolean;
8-
isValid: boolean;
9-
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
5+
contact: ContactState
6+
errors: Partial<ContactState>
7+
submitting: boolean
8+
submitStatus: 'idle' | 'success' | 'error'
9+
submitMessage: string
10+
isValid: boolean
11+
onSubmit: (event: FormEvent<HTMLFormElement>) => void
1012
onFieldChange: <K extends keyof ContactState>(
1113
key: K,
1214
value: ContactState[K],
13-
) => void;
14-
};
15+
) => void
16+
}
1517

1618
const contactTopics = [
1719
'Project inquiry',
@@ -23,7 +25,9 @@ const contactTopics = [
2325
export function ContactBriefForm({
2426
contact,
2527
errors,
26-
sent,
28+
submitting,
29+
submitStatus,
30+
submitMessage,
2731
isValid,
2832
onSubmit,
2933
onFieldChange,
@@ -126,20 +130,26 @@ export function ContactBriefForm({
126130
<button
127131
className="button solid button-fit"
128132
type="submit"
129-
disabled={!isValid}
133+
disabled={!isValid || submitting}
134+
aria-busy={submitting}
130135
>
131-
Prepare Email Brief
136+
{submitting ? 'Sending...' : 'Prepare Email Brief'}
132137
</button>
133138
<p className="meta">
134-
Your details are only used to pre-fill your email app. Nothing is
135-
stored.
139+
Your details are only used to send your brief. Nothing is stored.
136140
</p>
137141

138-
{sent ? (
142+
{submitStatus === 'success' && (
139143
<p className="form-status" role="status">
140-
Mail app opened with your prepared brief.
144+
{submitMessage}
141145
</p>
142-
) : null}
146+
)}
147+
148+
{submitStatus === 'error' && (
149+
<p className="form-status error" role="alert">
150+
{submitMessage}
151+
</p>
152+
)}
143153
</form>
144154
);
145155
}

src/components/ContentRenderer.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@ export function ContentRenderer({ blocks }: { blocks: ContentBlock[] }) {
2626
case 'image': {
2727
const imageSrc = sanitizeUrl(block.value);
2828
if (!imageSrc) return null;
29+
30+
// Add responsive srcset for Unsplash images
31+
let srcSet = ''
32+
if (imageSrc.includes('unsplash.com')) {
33+
srcSet = `${imageSrc}?w=400 400w, ${imageSrc}?w=800 800w, ${imageSrc}?w=1200 1200w`
34+
}
35+
2936
return (
3037
<figure key={index} className="media-block">
3138
<img
3239
src={imageSrc}
40+
srcSet={srcSet}
41+
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 100vw"
3342
alt={block.caption || 'Project asset'}
3443
loading="lazy"
3544
/>

src/components/ResumePreview.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useState, useEffect } from 'react'
2+
import type { ReactNode } from 'react'
3+
import { useFocusTrap } from '../hooks/useFocusTrap'
4+
5+
type ResumePreviewProps = {
6+
resumeUrl: string
7+
children?: ReactNode
8+
}
9+
10+
export function ResumePreview({ resumeUrl, children }: ResumePreviewProps) {
11+
const [isOpen, setIsOpen] = useState(false)
12+
const focusTrapRef = useFocusTrap()
13+
14+
// Close modal on Escape key
15+
useEffect(() => {
16+
if (!isOpen) return
17+
18+
const handleKeyDown = (e: KeyboardEvent) => {
19+
if (e.key === 'Escape') {
20+
setIsOpen(false)
21+
}
22+
}
23+
24+
document.addEventListener('keydown', handleKeyDown)
25+
return () => document.removeEventListener('keydown', handleKeyDown)
26+
}, [isOpen])
27+
28+
return (
29+
<>
30+
<button className="button ghost" type="button" onClick={() => setIsOpen(true)}>
31+
{children || 'Preview Resume'}
32+
</button>
33+
34+
{isOpen && (
35+
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
36+
<div
37+
ref={focusTrapRef}
38+
className="modal-content resume-modal"
39+
onClick={(e) => e.stopPropagation()}
40+
role="dialog"
41+
aria-modal="true"
42+
aria-labelledby="resume-modal-title"
43+
>
44+
<div className="modal-header">
45+
<h2 id="resume-modal-title">Resume Preview</h2>
46+
<button
47+
className="modal-close"
48+
type="button"
49+
onClick={() => setIsOpen(false)}
50+
aria-label="Close resume preview"
51+
>
52+
53+
</button>
54+
</div>
55+
<div className="modal-body">
56+
<embed src={resumeUrl} type="application/pdf" />
57+
</div>
58+
<div className="modal-footer">
59+
<a href={resumeUrl} download className="button solid">
60+
Download PDF
61+
</a>
62+
</div>
63+
</div>
64+
</div>
65+
)}
66+
</>
67+
)
68+
}

src/content/blogPosts.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,5 +328,89 @@
328328
"href": "/blog"
329329
}
330330
]
331+
},
332+
{
333+
"slug": "motorcycles-and-code",
334+
"title": "Motorcycles and Code: Why I Ride",
335+
"excerpt": "Reflections on the parallels between riding a motorcycle and building resilient systems. Both require focus, precision, and respect for the unexpected.",
336+
"publishedOn": "2026-02-20",
337+
"tags": ["Lifestyle", "Riding"],
338+
"readMinutes": 6,
339+
"body": [],
340+
"template": "story",
341+
"coverImage": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?auto=format&fit=crop&q=80&w=1400",
342+
"blocks": [
343+
{
344+
"type": "text",
345+
"value": "I bought my first motorcycle three years ago as an antidote to spending too much time in front of screens. What started as weekend rides has become a meditation practice."
346+
},
347+
{
348+
"type": "quote",
349+
"value": "On a bike, you can't think about yesterday or tomorrow. You are only in this moment.",
350+
"caption": "Riding principle"
351+
},
352+
{
353+
"type": "text",
354+
"value": "The parallels to backend systems are obvious: you need situational awareness, you can't ignore warning signs, and planning ahead keeps you alive. Same lessons, different context."
355+
},
356+
{
357+
"type": "text",
358+
"value": "I've learned as much about system reliability from riding in traffic as from any architecture textbook. Both demand humility."
359+
}
360+
]
361+
},
362+
{
363+
"slug": "bangalore-riding-guide",
364+
"title": "A Rider's Guide to Bangalore Traffic",
365+
"excerpt": "Notes from my commute: how to stay safe, sane, and on schedule in one of India's busiest cities.",
366+
"publishedOn": "2026-01-15",
367+
"tags": ["Riding", "Lifestyle", "Bangalore"],
368+
"readMinutes": 8,
369+
"body": [],
370+
"template": "story",
371+
"coverImage": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?auto=format&fit=crop&q=80&w=1400",
372+
"blocks": [
373+
{
374+
"type": "text",
375+
"value": "Bangalore traffic is often described as chaotic. It's not—it's simply different from Western traffic models. Once you understand the patterns, it's navigable."
376+
},
377+
{
378+
"type": "quote",
379+
"value": "The key is maintaining awareness, not speed."
380+
},
381+
{
382+
"type": "text",
383+
"value": "Over the last two years, I've distilled a few rules: Assume invisible. Use mirror discipline. Find your steady pace. The commute should not define your day—it should clear your head."
384+
},
385+
{
386+
"type": "text",
387+
"value": "Best riding happens early morning or late evening when the city breathes slower. Take a detour. The destination matters less than the ride."
388+
}
389+
]
390+
},
391+
{
392+
"slug": "balancing-code-and-caffeine",
393+
"title": "On Coffee, Focus, and Sustainable Pace",
394+
"excerpt": "A short essay on the engineer's eternal struggle: how to maintain energy and clarity without burning out.",
395+
"publishedOn": "2025-12-10",
396+
"tags": ["Lifestyle"],
397+
"readMinutes": 5,
398+
"body": [],
399+
"template": "minimal",
400+
"coverImage": "https://images.unsplash.com/photo-1559056199-641a0ac8b3f7?auto=format&fit=crop&q=80&w=1400",
401+
"blocks": [
402+
{
403+
"type": "text",
404+
"value": "I've tried timers, sprints, and scheduled breaks. Nothing beats knowing your energy curve and protecting the hours when your mind is clearest."
405+
},
406+
{
407+
"type": "quote",
408+
"value": "Consistency beats intensity."
409+
},
410+
{
411+
"type": "text",
412+
"value": "A sustainable pace looks ordinary. No all-nighters. No heroic sprints. Just steady, intentional work every day. The best engineers I know optimize for longevity, not for impressiveness."
413+
}
414+
]
331415
}
332416
]

0 commit comments

Comments
 (0)