From 6180cb326316dc6bfdbc6f898b175ee73f8fcaf2 Mon Sep 17 00:00:00 2001 From: Abhrxdi4p Date: Sat, 21 Feb 2026 01:55:39 +0530 Subject: [PATCH 1/2] fix: improve API safety, UID uniqueness, timezone handling, and race-condition control - Generate globally unique ICS UID using crypto/UUID to prevent collisions - Handle GitHub API rate limiting (403) with meaningful error messages - Add proper res.ok checks before JSON parsing to prevent unhandled errors - Remove hardcoded ist and dynamically resolve event timezone - Prevent race conditions in load more with request guards and loading state control --- src/components/common/blog/BlogList.tsx | 34 +++++++++++++++++++++---- src/lib/calendar-utils.ts | 25 +++++++++++++++++- src/lib/date-utils.ts | 27 +++++++++++++++++--- src/modules/contributors/index.tsx | 34 ++++++++++++++++++++++++- src/store/blogActions.ts | 29 +++++++++++++++++++++ src/utils/blog.ts | 27 ++++++++++++++++++-- 6 files changed, 163 insertions(+), 13 deletions(-) diff --git a/src/components/common/blog/BlogList.tsx b/src/components/common/blog/BlogList.tsx index 9b20091..176d61e 100644 --- a/src/components/common/blog/BlogList.tsx +++ b/src/components/common/blog/BlogList.tsx @@ -1,7 +1,7 @@ "use client"; // since this component has interactivity added this component as client component -import { useCallback, useMemo, useState, useTransition } from "react"; +import { useCallback, useMemo, useRef, useState, useTransition } from "react"; import Link from "next/link"; import { loadMoreBlogs } from "@/store/blogActions"; import { Blog, BlogResponse, BlogSectionProps, BlogTag } from "@/types/blog"; @@ -51,6 +51,10 @@ export default function BlogList({ const [selectedTags, setSelectedTags] = useState([]); const [showFilters, setShowFilters] = useState(false); const [isPending, startTransition] = useTransition(); + + // Ref to track if a fetch is currently in progress (prevents race conditions) + const isFetchingRef = useRef(false); + const t = useTranslations("Blog"); const { isMobile, isPad } = useDeviceDetail(); @@ -94,6 +98,15 @@ export default function BlogList({ const handleMoreBlogsCTA = (e: React.MouseEvent) => { e.preventDefault(); + + // Prevent concurrent requests (race condition guard) + if (isFetchingRef.current || isPending) { + return; + } + + // Mark fetch as in progress + isFetchingRef.current = true; + loadMoreBlogs(cursor, blogsToShow()) .then(({ posts: newBlogs, endCursor, error }) => { if (error) { @@ -118,11 +131,18 @@ export default function BlogList({ } return true; }); + startTransition(() => { - setBlogsResponse((prev) => ({ - data: [...prev.data, ...validNewBlogs], - error: null, - })); + setBlogsResponse((prev) => { + // Additional safety: check for duplicate IDs before merging + const existingIds = new Set(prev.data.map((b) => b.id)); + const uniqueNewBlogs = validNewBlogs.filter((blog) => !existingIds.has(blog.id)); + + return { + data: [...prev.data, ...uniqueNewBlogs], + error: null, + }; + }); setCursor(endCursor); }); }) @@ -133,6 +153,10 @@ export default function BlogList({ error: error instanceof Error ? error.message : "Failed to load blogs", })); }); + }) + .finally(() => { + // Always reset the fetch flag, even on error + isFetchingRef.current = false; }); }; diff --git a/src/lib/calendar-utils.ts b/src/lib/calendar-utils.ts index 0c434e1..b8681f9 100644 --- a/src/lib/calendar-utils.ts +++ b/src/lib/calendar-utils.ts @@ -38,6 +38,26 @@ export function generateOutlookCalendarUrl(event: CalendarEvent): string { return `${baseUrl}?${params.toString()}`; } +/** + * Generate a globally unique identifier for calendar events + * Uses crypto.randomUUID() for guaranteed uniqueness + * @returns UUID string + */ +function generateEventUID(): string { + // Use Web Crypto API for cryptographically secure UUID + // Fallback to timestamp-based UUID if crypto is not available (older browsers) + if (typeof crypto !== "undefined" && crypto.randomUUID) { + return crypto.randomUUID(); + } + + // Fallback: Generate RFC 4122 version 4 UUID manually + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + /** * Generate ICS (iCalendar) file content for download * @param event - Calendar event data @@ -48,12 +68,15 @@ export function generateICSContent(event: CalendarEvent): string { const startDate = formatDateForCalendar(event.startDateTime); const endDate = formatDateForCalendar(event.endDateTime); + // Generate a globally unique UID to prevent calendar collisions + const uniqueUID = generateEventUID(); + return [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//React Kolkata//Event//EN", "BEGIN:VEVENT", - `UID:${event.title.replace(/\s+/g, "-").toLowerCase()}-${startDate}@react-kolkata.dev`, + `UID:${uniqueUID}@react-kolkata.dev`, `DTSTAMP:${now}`, `DTSTART:${startDate}`, `DTEND:${endDate}`, diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts index 967535b..8a4b5fa 100644 --- a/src/lib/date-utils.ts +++ b/src/lib/date-utils.ts @@ -151,25 +151,44 @@ function getRelativeTime(date: Date): string { * Format event time range from two ISO datetime strings * @param startDateTime - ISO datetime string * @param endDateTime - ISO datetime string + * @param timezone - Optional timezone (e.g., "Asia/Kolkata"). If not provided, uses user's local timezone * @returns Formatted time range (e.g., "11:00 AM – 1:00 PM IST") */ -export function formatEventTimeRange(startDateTime: string, endDateTime: string): string { +export function formatEventTimeRange( + startDateTime: string, + endDateTime: string, + timezone?: string +): string { const start = new Date(startDateTime); const end = new Date(endDateTime); + // Determine which timezone to use + const timeZone = timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; + + // Format times in the specified timezone const startTime = start.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, + timeZone, }); const endTime = end.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, + timeZone, }); - return `${startTime} – ${endTime} IST`; + // Get timezone abbreviation (e.g., "IST", "PST", "EST") + const timezoneAbbr = new Intl.DateTimeFormat("en-US", { + timeZone, + timeZoneName: "short", + }) + .formatToParts(start) + .find((part) => part.type === "timeZoneName")?.value || ""; + + return `${startTime} – ${endTime} ${timezoneAbbr}`; } // Convenience functions for common use cases @@ -183,5 +202,5 @@ export const formatBlogRelativeTime = (isoDateString: string) => // Event-specific convenience functions export const formatEventDate = (isoDateString: string) => formatDate(isoDateString, DATE_FORMATS.BLOG_DATE); -export const formatEventTime = (startDateTime: string, endDateTime: string) => - formatEventTimeRange(startDateTime, endDateTime); +export const formatEventTime = (startDateTime: string, endDateTime: string, timezone?: string) => + formatEventTimeRange(startDateTime, endDateTime, timezone); diff --git a/src/modules/contributors/index.tsx b/src/modules/contributors/index.tsx index 2b592b9..f9a1d28 100644 --- a/src/modules/contributors/index.tsx +++ b/src/modules/contributors/index.tsx @@ -36,7 +36,39 @@ const ContributorsSection = () => { ); if (!response.ok) { - throw new Error(`Failed to fetch contributors: ${response.status}`); + // Handle specific HTTP error codes with meaningful messages + if (response.status === 403) { + // Check if it's a rate limit error + const rateLimitRemaining = response.headers.get("X-RateLimit-Remaining"); + const rateLimitReset = response.headers.get("X-RateLimit-Reset"); + + if (rateLimitRemaining === "0" && rateLimitReset) { + const resetTime = new Date(parseInt(rateLimitReset) * 1000); + const resetTimeFormatted = resetTime.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + throw new Error( + `GitHub API rate limit exceeded. Please try again after ${resetTimeFormatted}. You can also authenticate with GitHub to increase your rate limit.` + ); + } + throw new Error( + "Access forbidden. This might be due to API rate limiting or access restrictions." + ); + } else if (response.status === 404) { + throw new Error( + "Repository not found. Please check if the repository exists and is public." + ); + } else if (response.status >= 500) { + throw new Error( + "GitHub servers are experiencing issues. Please try again later." + ); + } else { + throw new Error( + `Unable to fetch contributors (Error ${response.status}). Please try again.` + ); + } } const data = await response.json(); diff --git a/src/store/blogActions.ts b/src/store/blogActions.ts index 6099de4..ff2bf62 100644 --- a/src/store/blogActions.ts +++ b/src/store/blogActions.ts @@ -27,7 +27,36 @@ export async function loadMoreBlogs( variables: { postCount: count, cursor: cursor.toString() }, }), }); + + // Check response status before attempting to parse JSON + if (!res.ok) { + // Attempt to parse error response for more details + let errorMessage = `Failed to fetch blogs (HTTP ${res.status})`; + try { + const errorData = await res.json(); + if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors[0]?.message) { + errorMessage = errorData.errors[0].message; + } + } catch { + // If error response is not JSON, use status-based message + if (res.status === 429) { + errorMessage = "Too many requests. Please try again in a moment."; + } else if (res.status >= 500) { + errorMessage = "Blog service is temporarily unavailable. Please try again later."; + } else if (res.status === 403) { + errorMessage = "Access denied to blog service."; + } + } + throw new Error(errorMessage); + } + const { data }: { data: { publication: HashnodePublication } } = await res.json(); + + // Validate API response structure + if (!data || !data.publication || !data.publication.posts) { + throw new Error("Invalid response structure from blog API"); + } + const { edges, pageInfo } = data.publication.posts; const posts: Blog[] = edges.map((edge) => edge.node); diff --git a/src/utils/blog.ts b/src/utils/blog.ts index 44d887f..abb15d8 100644 --- a/src/utils/blog.ts +++ b/src/utils/blog.ts @@ -19,10 +19,33 @@ export async function getInitialBlogs(): Promise { }); if (!res.ok) { - // This will be caught by the error boundary - throw new Error("Failed to fetch blogs"); + // Provide meaningful error messages based on status code + let errorMessage = `Failed to fetch blogs (HTTP ${res.status})`; + try { + const errorData = await res.json(); + if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors[0]?.message) { + errorMessage = errorData.errors[0].message; + } + } catch { + // If error response is not JSON, use status-based message + if (res.status === 429) { + errorMessage = "Too many requests. Please try again in a moment."; + } else if (res.status >= 500) { + errorMessage = "Blog service is temporarily unavailable. Please try again later."; + } else if (res.status === 403) { + errorMessage = "Access denied to blog service."; + } + } + throw new Error(errorMessage); } + const { data }: HashnodeAPIResponse = await res.json(); + + // Validate API response structure + if (!data || !data.publication || !data.publication.posts) { + throw new Error("Invalid response structure from blog API"); + } + const { edges, pageInfo } = data.publication.posts; const posts: Blog[] = edges.map((edge) => edge.node); const endCursor = pageInfo.hasNextPage ? pageInfo.endCursor : null; From 21a91ffc638d5a07e77436393741e03d5c9859c0 Mon Sep 17 00:00:00 2001 From: Abhrxdi4p Date: Sun, 22 Feb 2026 19:13:38 +0530 Subject: [PATCH 2/2] Fixed issues --- src/lib/date-utils.ts | 2 +- src/modules/contributors/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts index 8a4b5fa..7ed15f9 100644 --- a/src/lib/date-utils.ts +++ b/src/lib/date-utils.ts @@ -181,7 +181,7 @@ export function formatEventTimeRange( }); // Get timezone abbreviation (e.g., "IST", "PST", "EST") - const timezoneAbbr = new Intl.DateTimeFormat("en-US", { + const timezoneAbbr = new Intl.DateTimeFormat("en-IN", { timeZone, timeZoneName: "short", }) diff --git a/src/modules/contributors/index.tsx b/src/modules/contributors/index.tsx index f9a1d28..7597be2 100644 --- a/src/modules/contributors/index.tsx +++ b/src/modules/contributors/index.tsx @@ -50,7 +50,7 @@ const ContributorsSection = () => { hour12: true, }); throw new Error( - `GitHub API rate limit exceeded. Please try again after ${resetTimeFormatted}. You can also authenticate with GitHub to increase your rate limit.` + `GitHub API rate limit exceeded. Please try again after ${resetTimeFormatted}.` ); } throw new Error(