Skip to content

Commit 249b359

Browse files
committed
feat: add new responsive css and logic for like and share buttons on blogposts
1 parent 9a14c2b commit 249b359

File tree

8 files changed

+299
-36
lines changed

8 files changed

+299
-36
lines changed

analytics-worker/src/worker.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,101 @@ async function getAnalyticsFromR2(env, startDate, endDate) {
565565
};
566566
}
567567

568+
// Handle blog like count requests
569+
async function handleBlogLikeCount(request, env, dynamicCorsHeaders = corsHeaders) {
570+
try {
571+
const url = new URL(request.url);
572+
const pathParts = url.pathname.split('/');
573+
574+
// Extract post ID from /api/blog/{postId}/likes
575+
if (pathParts.length !== 5 || pathParts[1] !== 'api' || pathParts[2] !== 'blog' || pathParts[4] !== 'likes') {
576+
return new Response('Invalid blog likes endpoint format', {
577+
status: 400,
578+
headers: dynamicCorsHeaders
579+
});
580+
}
581+
582+
const postId = decodeURIComponent(pathParts[3]);
583+
console.log('Fetching like count for post:', postId);
584+
585+
// Query R2 for blog engagement events for this post
586+
const bucket = env.ANALYTICS_BUCKET;
587+
if (!bucket) {
588+
console.error('R2 bucket not configured');
589+
return new Response(JSON.stringify({ likes: 0, error: 'Storage not configured' }), {
590+
headers: {
591+
'Content-Type': 'application/json',
592+
...dynamicCorsHeaders
593+
}
594+
});
595+
}
596+
597+
// List all analytics files to find blog engagement events
598+
let likeCount = 0;
599+
try {
600+
const objects = await bucket.list({ prefix: 'analytics/' });
601+
602+
for (const obj of objects.objects) {
603+
try {
604+
const eventFile = await bucket.get(obj.key);
605+
if (eventFile) {
606+
const eventText = await eventFile.text();
607+
const events = eventText.split('\n').filter(line => line.trim());
608+
609+
for (const eventLine of events) {
610+
try {
611+
const event = JSON.parse(eventLine);
612+
613+
// Count 'like' events for this specific post
614+
if (event.type === 'blog_engagement' &&
615+
event.action === 'like' &&
616+
event.postId === postId) {
617+
likeCount++;
618+
}
619+
} catch (parseError) {
620+
// Skip invalid JSON lines
621+
continue;
622+
}
623+
}
624+
}
625+
} catch (fileError) {
626+
// Skip files we can't read
627+
console.log('Could not read file:', obj.key, fileError.message);
628+
continue;
629+
}
630+
}
631+
} catch (listError) {
632+
console.error('Error querying R2 for like count:', listError);
633+
// Return 0 likes if we can't query the storage
634+
}
635+
636+
console.log(`Found ${likeCount} likes for post: ${postId}`);
637+
638+
return new Response(JSON.stringify({
639+
likes: likeCount,
640+
postId: postId
641+
}), {
642+
headers: {
643+
'Content-Type': 'application/json',
644+
...dynamicCorsHeaders
645+
}
646+
});
647+
648+
} catch (error) {
649+
console.error('Blog like count error:', error);
650+
return new Response(JSON.stringify({
651+
likes: 0,
652+
error: 'Internal server error'
653+
}), {
654+
status: 500,
655+
headers: {
656+
'Content-Type': 'application/json',
657+
...dynamicCorsHeaders
658+
}
659+
});
660+
}
661+
}
662+
568663
const worker = {
569664
async fetch(request, env) {
570665
const url = new URL(request.url);
@@ -586,6 +681,9 @@ const worker = {
586681
} else if (url.pathname === '/api/dashboard' && request.method === 'GET') {
587682
// Dashboard data endpoint (future feature)
588683
return new Response('Dashboard API endpoint', { headers: dynamicCorsHeaders });
684+
} else if (url.pathname.startsWith('/api/blog/') && url.pathname.endsWith('/likes') && request.method === 'GET') {
685+
// Blog like count endpoint
686+
return handleBlogLikeCount(request, env, dynamicCorsHeaders);
589687
} else {
590688
return new Response('Not found', {
591689
status: 404,

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
object-src 'none';
1515
img-src 'self' data: https://cdn.ujjwalvivek.com https://images.unsplash.com;
1616
font-src 'self' https://fonts.gstatic.com;
17-
connect-src 'self' https://challenges.cloudflare.com https://*.cloudflare.com https://api.github.com https://cdn.ujjwalvivek.com https://api.emailjs.com https://analytics.ujjwalvivek.com/api;
17+
connect-src 'self' https://challenges.cloudflare.com https://*.cloudflare.com https://api.github.com https://api.github.com/* https://cdn.ujjwalvivek.com https://api.emailjs.com https://analytics.ujjwalvivek.com;
1818
frame-src https://challenges.cloudflare.com https://*.notion.site https://www.youtube.com https://www.youtube-nocookie.com;
1919
worker-src 'self' blob:;"
2020
/>

public/posts/ideas.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
## 📅 v1.4 Target Changelog (WIP)
22

33
#### Ongoing
4-
* [x] Custom made Analytics software, with a focus on privacy and performance. `Architecture planned, implementation ready`
5-
* [ ] update look and feel with some advanced css features.
6-
* [ ] Revisit UX of the website.
74

85
#### Optional
6+
* [ ] update look and feel with some advanced css features.
7+
* [ ] Revisit UX of the website.
98
* [ ] Editable customised shortcut keys.
109
* [ ] Bundle some cool npm packages
1110

@@ -33,4 +32,5 @@
3332
`Make it feel like sending a message through a shell (how to embed subject and body in mailto?)`
3433
* [x] Project sorting (opinionated) and search functionality - via commandpalette.
3534
* [x] Terminal command palette - Ctrl+K style navigation across your whole site (optional)
36-
* [x] Shortcuts Overlay with a helper toast
35+
* [x] Shortcuts Overlay with a helper toast
36+
* [x] Custom made Analytics software, with a focus on privacy and performance. `Architecture planned, implementation ready`

public/posts/log_0000_boot_sequence.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ But because abandoned builds taught my brain:
163163
I didn't want to be that guy anymore.
164164
So I pushed. Not because it's perfect, but because it's mine.
165165
166-
<Note title="This is not a 2-week build."> This is a **two-year transformation log** disguised as a terminal vibe. </Note>
166+
This is a **two-year transformation log** disguised as a terminal vibe.
167167
168168
```javascript
169169
> deploy

src/components/Pages/Blog/Blog.module.css

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,121 @@
4646
box-shadow: 0 2px 4px rgba(var(--background-color-rgb), 0.2);
4747
}
4848

49+
.copyButton:hover {
50+
background: var(--background-color);
51+
color: var(--primary-color);
52+
transform: translateY(-1px);
53+
}
54+
55+
/* Blog Actions Styles */
56+
.blogActions {
57+
display: flex;
58+
align-items: center;
59+
gap: 1rem;
60+
position: relative;
61+
font-family: var(--font-mono);
62+
}
63+
64+
.actionButton {
65+
display: flex;
66+
align-items: center;
67+
border: none;
68+
background: transparent;
69+
color: var(--text-color);
70+
font-family: var(--font-mono);
71+
cursor: pointer;
72+
transition: all 0.2s ease;
73+
justify-content: center;
74+
position: relative;
75+
overflow: hidden;
76+
}
77+
78+
.actionButton:disabled {
79+
opacity: 0.7;
80+
cursor: not-allowed;
81+
transform: none;
82+
}
83+
84+
.likeButton:hover {
85+
color: var(--status-ram-color);
86+
opacity: 1;
87+
}
88+
89+
.likeButton.liked {
90+
color: var(--status-ram-color);
91+
}
92+
93+
.likeButton.liked:hover {
94+
opacity: 0.9;
95+
transform: none;
96+
cursor: not-allowed;
97+
color: var(--status-ram-color);
98+
}
99+
100+
.shareButton {
101+
background: transparent;
102+
border-color: none;
103+
opacity: 0.8;
104+
}
105+
106+
.shareButton:hover {
107+
color: var(--status-warn-color);
108+
opacity: 1;
109+
}
110+
111+
.buttonIcon {
112+
font-size: 1.5rem;
113+
line-height: 1;
114+
}
115+
116+
/* Copy Notification */
117+
.copyNotification {
118+
position: absolute;
119+
top: 0px;
120+
left: 50%;
121+
transform: translateX(-50%);
122+
backdrop-filter: blur(32px) saturate(180%);
123+
-webkit-backdrop-filter: blur(32px) saturate(180%);
124+
background: rgba(var(--background-color-rgb), 0.3); color: var(--background-color);
125+
padding: 0.5rem 1rem;
126+
border-radius: 2px;
127+
font-family: var(--font-mono);
128+
font-size: 0.8rem;
129+
font-weight: 400;
130+
display: flex;
131+
align-items: center;
132+
gap: 0.5rem;
133+
animation: slideInBounce 0.4s ease-out;
134+
z-index: 100;
135+
color: var(--text-color);
136+
}
137+
138+
.notificationText {
139+
white-space: nowrap;
140+
}
141+
142+
@keyframes slideInBounce {
143+
0% {
144+
opacity: 0;
145+
transform: translateX(-50%) translateY(10px);
146+
}
147+
60% {
148+
opacity: 1;
149+
transform: translateX(-50%) translateY(-5px);
150+
}
151+
100% {
152+
opacity: 1;
153+
transform: translateX(-50%) translateY(0);
154+
}
155+
}
156+
157+
@keyframes sparkle {
158+
0%, 100% { transform: scale(1) rotate(0deg); }
159+
25% { transform: scale(1.2) rotate(90deg); }
160+
50% { transform: scale(1.1) rotate(180deg); }
161+
75% { transform: scale(1.2) rotate(270deg); }
162+
}
163+
49164
.copyButton:hover {
50165
background: var(--background-color);
51166
transform: translateY(-1px);

src/components/Pages/Blog/BlogPost.js

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { BsFillPencilFill, BsFillInfoSquareFill } from "react-icons/bs";
2121
import { MdDangerous } from "react-icons/md";
2222
import { AiTwotoneStar } from "react-icons/ai";
2323
import { useBlogAnalytics } from '../../../hooks/useAnalytics';
24+
import { AiFillLike, AiOutlineLike } from "react-icons/ai";
25+
import { FaShare } from "react-icons/fa";
2426

2527
// Simple reading time calculation function
2628
const calculateReadingTime = (text) => {
@@ -146,8 +148,32 @@ const BlogPost = () => {
146148
const postId = post.data?.id || filename;
147149
const postTitle = post.data?.title || 'Loading...';
148150

149-
const { trackLike, trackShare, hasLiked, hasShared } = useBlogAnalytics(postId, postTitle);
151+
const { trackLike, trackShare, hasLiked } = useBlogAnalytics(postId, postTitle);
150152

153+
// State for copy notification
154+
const [showCopyNotification, setShowCopyNotification] = useState(false);
155+
156+
157+
// Function to copy blog link and show notification
158+
const handleShare = async () => {
159+
const blogUrl = `${window.location.origin}/blog/${filename}`;
160+
161+
try {
162+
await navigator.clipboard.writeText(blogUrl);
163+
setShowCopyNotification(true);
164+
trackShare('clipboard');
165+
166+
// Hide notification after 3 seconds
167+
setTimeout(() => {
168+
setShowCopyNotification(false);
169+
}, 3000);
170+
} catch (error) {
171+
console.error('Failed to copy:', error);
172+
// Fallback: show URL in a prompt for manual copy
173+
prompt('Copy this URL:', blogUrl);
174+
trackShare('manual');
175+
}
176+
};
151177

152178
useEffect(() => {
153179
const fetchPost = async () => {
@@ -268,21 +294,45 @@ const BlogPost = () => {
268294
{post.content}
269295
</ReactMarkdown>
270296
</div>
297+
<div className={styles.blogActions}>
298+
<button
299+
className={`${styles.actionButton} ${styles.likeButton} ${hasLiked ? styles.liked : ''}`}
300+
onClick={trackLike}
301+
disabled={hasLiked}
302+
title={hasLiked ? "You've already liked this post!" : "Like this post!"}
303+
>
304+
<span className={styles.buttonIcon}>
305+
{hasLiked ? <AiFillLike /> : <AiOutlineLike />}
306+
</span>
307+
</button>
308+
309+
<button
310+
className={`${styles.actionButton} ${styles.shareButton}`}
311+
onClick={handleShare}
312+
title="Copy link to clipboard"
313+
>
314+
<span className={styles.buttonIcon}>
315+
<FaShare />
316+
</span>
317+
</button>
318+
319+
{/* Copy notification */}
320+
{showCopyNotification && (
321+
<div className={styles.copyNotification}>
322+
<span className={styles.notificationText}>
323+
Copied to clipboard!
324+
</span>
325+
</div>
326+
)}
327+
</div>
271328
<div className={styles.authorSignature}>
272329
<p>
273330
<strong>Vivek</strong> crafting systems,
274331
<br />
275332
one line at a time.
276333
</p>
277334
</div>
278-
<div className={styles.blogActions}>
279-
<button onClick={trackLike} disabled={hasLiked} style={{opacity: hasLiked ? 0.5 : 1}}>
280-
{hasLiked ? '👍 Liked!' : '👍 Like'}
281-
</button>
282-
<button onClick={() => trackShare('manual')} disabled={hasShared} style={{opacity: hasShared ? 0.5 : 1}}>
283-
{hasShared ? '🔗 Shared!' : '🔗 Share'}
284-
</button>
285-
</div>
335+
286336
<br />
287337
<RelatedPosts posts={relatedPosts} />
288338
</div>

0 commit comments

Comments
 (0)