From 4d671927da785b8c2e0d0ac224650d08b70e0c91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:01:48 +0000 Subject: [PATCH 1/4] Initial plan From 2083442f59c77d304299bbfd18331c698c733914 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:13:24 +0000 Subject: [PATCH 2/4] Refactor duplicated code: create reusable date formatter hook and breadcrumb component Co-authored-by: marcelo-m7 <117441129+marcelo-m7@users.noreply.github.com> --- src/components/PageBreadcrumb.tsx | 70 +++++++++++++++++++++++ src/components/blog/CommentsSection.tsx | 31 +++------- src/hooks/useLocalizedDateFormatter.ts | 74 ++++++++++++++++++++++++ src/pages/Blog.tsx | 53 +++-------------- src/pages/Contact.tsx | 42 +------------- src/pages/Dashboard.tsx | 47 ++-------------- src/pages/Projects.tsx | 75 ++++--------------------- src/pages/Solutions.tsx | 25 +-------- src/pages/blog/[slug].tsx | 24 ++------ 9 files changed, 184 insertions(+), 257 deletions(-) create mode 100644 src/components/PageBreadcrumb.tsx create mode 100644 src/hooks/useLocalizedDateFormatter.ts diff --git a/src/components/PageBreadcrumb.tsx b/src/components/PageBreadcrumb.tsx new file mode 100644 index 0000000..ae384ea --- /dev/null +++ b/src/components/PageBreadcrumb.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@/components/ui/breadcrumb'; + +interface BreadcrumbItem { + label: string; + path?: string; +} + +interface PageBreadcrumbProps { + currentPage: string; + items?: BreadcrumbItem[]; + className?: string; +} + +/** + * Reusable breadcrumb navigation component + * @param currentPage - Translation key for current page (e.g., 'navigation.blog') + * @param items - Optional array of intermediate breadcrumb items + * @param className - Optional CSS classes for container + */ +export const PageBreadcrumb = ({ + currentPage, + items = [], + className = 'max-w-7xl mx-auto px-4 pt-4' +}: PageBreadcrumbProps) => { + const { t } = useTranslation(); + + return ( +
+ + + + + {t('navigation.home')} + + + + {items.map((item, index) => ( + + + + {item.path ? ( + + {item.label} + + ) : ( + {item.label} + )} + + + ))} + + + + {t(currentPage)} + + + +
+ ); +}; diff --git a/src/components/blog/CommentsSection.tsx b/src/components/blog/CommentsSection.tsx index b7bcdec..4e7947e 100644 --- a/src/components/blog/CommentsSection.tsx +++ b/src/components/blog/CommentsSection.tsx @@ -5,13 +5,13 @@ import { supabase } from '@/integrations/supabase'; import type { Database } from '@/integrations/supabase/types'; import { useAuth } from '@/hooks/useAuth'; import { useToast } from '@/hooks/use-toast'; +import { useLocalizedDateFormatter } from '@/hooks/useLocalizedDateFormatter'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Loader2 } from 'lucide-react'; import { Link } from 'react-router-dom'; -import { getNormalizedLocale } from '@/lib/i18n'; interface CommentsSectionProps { postId: string; @@ -44,31 +44,16 @@ const getInitials = (name: string) => { }; const CommentsSection = ({ postId }: CommentsSectionProps) => { - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); const { user, isLoading: isAuthLoading } = useAuth(); const { toast } = useToast(); const queryClient = useQueryClient(); const [newComment, setNewComment] = useState(''); - const normalizedLocale = useMemo( - () => getNormalizedLocale(i18n.language), - [i18n.language] - ); - - const dateFormatter = useMemo(() => { - try { - return new Intl.DateTimeFormat(normalizedLocale, { - dateStyle: 'medium', - timeStyle: 'short', - }); - } catch (error) { - console.error('Unsupported locale for comment date formatting', error); - return new Intl.DateTimeFormat('en-US', { - dateStyle: 'medium', - timeStyle: 'short', - }); - } - }, [normalizedLocale]); + const { formatDate } = useLocalizedDateFormatter({ + dateStyle: 'medium', + timeStyle: 'short', + }); const { data: comments, @@ -249,9 +234,7 @@ const CommentsSection = ({ postId }: CommentsSectionProps) => { comments?.map((comment) => { const authorName = comment.author?.name ?? t('blog.comments.anonymous'); - const formattedDate = dateFormatter.format( - new Date(comment.created_at) - ); + const formattedDate = formatDate(comment.created_at); return ( string; + dateFormatter: Intl.DateTimeFormat; +} + +/** + * Custom hook for localized date formatting with automatic fallback + * @param options - Intl.DateTimeFormat options + * @param fallbackOptions - Fallback options if normalized locale fails (defaults to en-US with same options) + * @returns Object containing formatDate function and dateFormatter instance + */ +export const useLocalizedDateFormatter = ( + options: DateFormatterOptions = { dateStyle: 'medium' }, + fallbackOptions?: DateFormatterOptions +): UseLocalizedDateFormatterReturn => { + const { i18n } = useTranslation(); + + const normalizedLocale = useMemo( + () => getNormalizedLocale(i18n.language), + [i18n.language] + ); + + const fallbackDateFormatter = useMemo( + () => + new Intl.DateTimeFormat('en-US', fallbackOptions ?? options), + [fallbackOptions, options] + ); + + const dateFormatter = useMemo(() => { + try { + return new Intl.DateTimeFormat(normalizedLocale, options); + } catch (error) { + console.error('Unsupported locale for date formatting', error); + return fallbackDateFormatter; + } + }, [normalizedLocale, options, fallbackDateFormatter]); + + const formatDate = useCallback( + (date: string | Date | null | undefined) => { + if (!date) return '—'; + + try { + const dateObject = typeof date === 'string' ? new Date(date) : date; + return dateFormatter.format(dateObject); + } catch (error) { + console.error('Error formatting date', error); + try { + const dateObject = typeof date === 'string' ? new Date(date) : date; + return fallbackDateFormatter.format(dateObject); + } catch { + return '—'; + } + } + }, + [dateFormatter, fallbackDateFormatter] + ); + + return { formatDate, dateFormatter }; +}; diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx index 76c4605..1326852 100644 --- a/src/pages/Blog.tsx +++ b/src/pages/Blog.tsx @@ -1,10 +1,11 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo } from 'react'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import Layout from '@/components/Layout'; import Meta from '@/components/Meta'; import NewsletterSection from '@/components/NewsletterSection'; +import { PageBreadcrumb } from '@/components/PageBreadcrumb'; import { ArrowRight, Clock, @@ -15,14 +16,6 @@ import { import { supabase } from '@/integrations/supabase'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { - Breadcrumb, - BreadcrumbList, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbPage, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb'; import { Pagination, PaginationContent, @@ -31,7 +24,7 @@ import { PaginationEllipsis, } from '@/components/ui/pagination'; import { cn } from '@/lib/utils'; -import { getNormalizedLocale } from '@/lib/i18n'; +import { useLocalizedDateFormatter } from '@/hooks/useLocalizedDateFormatter'; import type { Database } from '@/integrations/supabase/types'; const FALLBACK_IMAGE = @@ -91,26 +84,10 @@ const getPaginationRange = ( }; const Blog = () => { - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); const [page, setPage] = useState(1); - const normalizedLocale = useMemo( - () => getNormalizedLocale(i18n.language), - [i18n.language] - ); - - const dateFormatter = useMemo(() => { - try { - return new Intl.DateTimeFormat(normalizedLocale, { - dateStyle: 'medium', - }); - } catch (error) { - console.error('Unsupported locale for blog date formatting', error); - return new Intl.DateTimeFormat('en-US', { - dateStyle: 'medium', - }); - } - }, [normalizedLocale]); + const { formatDate } = useLocalizedDateFormatter({ dateStyle: 'medium' }); const categories = useMemo( () => BLOG_CATEGORY_KEYS.map((key) => t(key)), @@ -160,12 +137,12 @@ const Blog = () => { excerpt: post.excerpt ?? t('blog.fallbackExcerpt'), image: post.image_url ?? FALLBACK_IMAGE, author: t('blog.defaultAuthor'), - date: dateFormatter.format(new Date(post.updated_at)), + date: formatDate(post.updated_at), readTime: t('blog.readTimeDefault'), category: t('blog.defaultCategory'), featured: page === 1 && index === 0, })), - [data?.posts, dateFormatter, page, t] + [data?.posts, formatDate, page, t] ); const featuredPost = page === 1 ? formattedPosts[0] : undefined; @@ -240,21 +217,7 @@ const Blog = () => { ogDescription={t('blog.description')} ogImage="/placeholder.svg" /> -
- - - - - {t('navigation.home')} - - - - - {t('navigation.blog')} - - - -
+
diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx index 5ad0471..1d73561 100644 --- a/src/pages/Contact.tsx +++ b/src/pages/Contact.tsx @@ -5,19 +5,11 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import Layout from '@/components/Layout'; import Meta from '@/components/Meta'; +import { PageBreadcrumb } from '@/components/PageBreadcrumb'; import { Mail, Send, CheckCircle } from 'lucide-react'; import { supabase } from '@/integrations/supabase'; import { useToast } from '@/hooks/use-toast'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { - Breadcrumb, - BreadcrumbList, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbPage, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb'; import { useQuery } from '@tanstack/react-query'; const PROJECT_TYPE_KEYS = [ @@ -304,21 +296,7 @@ const Contact = () => { ogDescription={t('contact.description')} ogImage="/placeholder.svg" /> -
- - - - - {t('navigation.home')} - - - - - {t('navigation.contact')} - - - -
+
@@ -352,21 +330,7 @@ const Contact = () => { ogDescription={t('contact.description')} ogImage="/placeholder.svg" /> -
- - - - - {t('navigation.home')} - - - - - {t('navigation.contact')} - - - -
+ {/* Hero Section */}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index c77cb64..c4f3f04 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -4,6 +4,7 @@ import { supabase } from '@/integrations/supabase'; import type { Database } from '@/integrations/supabase/types'; import { useAuth } from '@/hooks/useAuth'; import { useToast } from '@/hooks/use-toast'; +import { useLocalizedDateFormatter } from '@/hooks/useLocalizedDateFormatter'; import { Button } from '@/components/ui/button'; import { Card, @@ -26,7 +27,6 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import Loading from '@/components/Loading'; import { AlertTriangle, Loader2, LogOut, RefreshCcw } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { getNormalizedLocale } from '@/lib/i18n'; type Profile = Database['public']['Tables']['profiles']['Row']; type Lead = Database['public']['Tables']['leads']['Row']; @@ -34,7 +34,7 @@ type NewsletterSubscriber = Database['public']['Tables']['newsletter_subscribers']['Row']; const Dashboard = () => { - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); const { user, signOut } = useAuth(); const { toast } = useToast(); const navigate = useNavigate(); @@ -149,45 +149,10 @@ const Dashboard = () => { void loadDashboard(); }, [loadDashboard]); - const normalizedLocale = useMemo( - () => getNormalizedLocale(i18n.language), - [i18n.language] - ); - - const fallbackDateFormatter = useMemo( - () => - new Intl.DateTimeFormat('en-US', { - dateStyle: 'short', - timeStyle: 'short', - }), - [] - ); - - const dateTimeFormatter = useMemo(() => { - try { - return new Intl.DateTimeFormat(normalizedLocale, { - dateStyle: 'short', - timeStyle: 'short', - }); - } catch (error) { - console.error('Unsupported locale for dashboard date formatting', error); - return fallbackDateFormatter; - } - }, [fallbackDateFormatter, normalizedLocale]); - - const formatDate = useCallback( - (value: string | null | undefined) => { - if (!value) return '—'; - - try { - return dateTimeFormatter.format(new Date(value)); - } catch (error) { - console.error('Error formatting dashboard date', error); - return fallbackDateFormatter.format(new Date(value)); - } - }, - [dateTimeFormatter, fallbackDateFormatter] - ); + const { formatDate } = useLocalizedDateFormatter({ + dateStyle: 'short', + timeStyle: 'short', + }); const handleRefresh = () => { void loadDashboard(); diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 1c3de55..eabae28 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -1,4 +1,4 @@ -import { useMemo, useCallback } from 'react'; +import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Github, ExternalLink, Calendar, ArrowRight } from 'lucide-react'; import Layout from '../components/Layout'; @@ -6,18 +6,12 @@ import Meta from '@/components/Meta'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { - Breadcrumb, - BreadcrumbList, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbPage, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb'; +import { PageBreadcrumb } from '@/components/PageBreadcrumb'; import { supabase } from '@/integrations/supabase'; import { useTranslation, Trans } from 'react-i18next'; import { Link } from 'react-router-dom'; import useRepositorySync from '@/hooks/useRepositorySync'; +import { useLocalizedDateFormatter } from '@/hooks/useLocalizedDateFormatter'; import { fetchSupabaseSolutions, getFallbackSolutions, @@ -25,7 +19,6 @@ import { } from '@/lib/solutions'; import type { GitHubRepository } from '@/lib/solutions'; import type { SolutionContent } from '@/types/solutions'; -import { getNormalizedLocale } from '@/lib/i18n'; import { SolutionCard, sectionContainer, sectionPaddingY } from '@monynha/ui'; import { cn } from '@/lib/utils'; @@ -43,50 +36,16 @@ const GITHUB_REPOS_URL = 'https://api.github.com/orgs/Monynha-Softwares/repos?per_page=100'; const Projects = () => { - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); useRepositorySync(); const memoizedFallbackSolutions = useMemo(() => getFallbackSolutions(), []); - const normalizedLocale = useMemo( - () => getNormalizedLocale(i18n.language), - [i18n.language] - ); - - const fallbackDateFormatter = useMemo( - () => - new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }), - [] - ); - - const dateFormatter = useMemo(() => { - try { - return new Intl.DateTimeFormat(normalizedLocale, { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - } catch (error) { - console.error('Unsupported locale for project date formatting', error); - return fallbackDateFormatter; - } - }, [fallbackDateFormatter, normalizedLocale]); - - const formatDate = useCallback( - (dateString: string) => { - try { - return dateFormatter.format(new Date(dateString)); - } catch (error) { - console.error('Error formatting project date', error); - return fallbackDateFormatter.format(new Date(dateString)); - } - }, - [dateFormatter, fallbackDateFormatter] - ); + const { formatDate } = useLocalizedDateFormatter({ + year: 'numeric', + month: 'short', + day: 'numeric', + }); const { data: repositories = [], @@ -251,21 +210,7 @@ const Projects = () => { ogDescription={t('projects.description')} ogImage="/placeholder.svg" /> -
- - - - - {t('navigation.home')} - - - - - {t('navigation.projects')} - - - -
+
diff --git a/src/pages/Solutions.tsx b/src/pages/Solutions.tsx index ce173c0..f4f3eef 100644 --- a/src/pages/Solutions.tsx +++ b/src/pages/Solutions.tsx @@ -3,15 +3,8 @@ import { useQuery } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import Layout from '@/components/Layout'; import Meta from '@/components/Meta'; +import { PageBreadcrumb } from '@/components/PageBreadcrumb'; import { Link } from 'react-router-dom'; -import { - Breadcrumb, - BreadcrumbList, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbPage, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb'; import { ArrowRight } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { supabase } from '@/integrations/supabase'; @@ -102,21 +95,7 @@ const Solutions = () => { ogDescription={t('solutionsPage.description')} ogImage="/placeholder.svg" /> -
- - - - - {t('navigation.home')} - - - - - {t('navigation.solutions')} - - - -
+ {/* Hero Section */}
diff --git a/src/pages/blog/[slug].tsx b/src/pages/blog/[slug].tsx index 42a3727..f7bb750 100644 --- a/src/pages/blog/[slug].tsx +++ b/src/pages/blog/[slug].tsx @@ -8,7 +8,7 @@ import Meta from '@/components/Meta'; import NewsletterSection from '@/components/NewsletterSection'; import CommentsSection from '@/components/blog/CommentsSection'; import { supabase } from '@/integrations/supabase'; -import { getNormalizedLocale } from '@/lib/i18n'; +import { useLocalizedDateFormatter } from '@/hooks/useLocalizedDateFormatter'; import type { Database } from '@/integrations/supabase/types'; import { Breadcrumb, @@ -227,23 +227,9 @@ const renderBlogContent = (content: string): ReactNode[] => { const BlogPostPage = () => { const { slug } = useParams<{ slug: string }>(); - const { t, i18n } = useTranslation(); - - const normalizedLocale = useMemo( - () => getNormalizedLocale(i18n.language), - [i18n.language] - ); + const { t } = useTranslation(); - const dateFormatter = useMemo(() => { - try { - return new Intl.DateTimeFormat(normalizedLocale, { - dateStyle: 'long', - }); - } catch (error) { - console.error('Unsupported locale for blog post date formatting', error); - return new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }); - } - }, [normalizedLocale]); + const { formatDate } = useLocalizedDateFormatter({ dateStyle: 'long' }); const { data: post, @@ -279,9 +265,7 @@ const BlogPostPage = () => { [post?.content] ); - const updatedDate = post - ? dateFormatter.format(new Date(post.updated_at)) - : ''; + const updatedDate = post ? formatDate(post.updated_at) : ''; const fallbackMetaTitle = t('blog.metaTitle'); const metaTitle = post From 23a22a4c6ec5219c906700b751ee1184379a8dad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:15:42 +0000 Subject: [PATCH 3/4] Address code review feedback: improve useLocalizedDateFormatter with configurable options Co-authored-by: marcelo-m7 <117441129+marcelo-m7@users.noreply.github.com> --- src/components/blog/CommentsSection.tsx | 6 ++- src/hooks/useLocalizedDateFormatter.ts | 52 ++++++++++++++++++------- src/pages/Blog.tsx | 4 +- src/pages/Dashboard.tsx | 6 ++- src/pages/Projects.tsx | 8 ++-- src/pages/blog/[slug].tsx | 4 +- 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/components/blog/CommentsSection.tsx b/src/components/blog/CommentsSection.tsx index 4e7947e..b202378 100644 --- a/src/components/blog/CommentsSection.tsx +++ b/src/components/blog/CommentsSection.tsx @@ -51,8 +51,10 @@ const CommentsSection = ({ postId }: CommentsSectionProps) => { const [newComment, setNewComment] = useState(''); const { formatDate } = useLocalizedDateFormatter({ - dateStyle: 'medium', - timeStyle: 'short', + dateOptions: { + dateStyle: 'medium', + timeStyle: 'short', + }, }); const { diff --git a/src/hooks/useLocalizedDateFormatter.ts b/src/hooks/useLocalizedDateFormatter.ts index 3b38748..8d292f7 100644 --- a/src/hooks/useLocalizedDateFormatter.ts +++ b/src/hooks/useLocalizedDateFormatter.ts @@ -13,21 +13,46 @@ interface DateFormatterOptions { day?: 'numeric' | '2-digit'; } +interface UseLocalizedDateFormatterOptions { + /** + * Intl.DateTimeFormat options for date formatting + */ + dateOptions?: DateFormatterOptions; + /** + * Fallback options if normalized locale fails (defaults to en-US with same options) + */ + fallbackOptions?: DateFormatterOptions; + /** + * String to return when date is null/undefined (defaults to '—') + */ + nullFallback?: string; +} + interface UseLocalizedDateFormatterReturn { formatDate: (date: string | Date | null | undefined) => string; dateFormatter: Intl.DateTimeFormat; } +/** + * Helper function to convert date input to Date object + */ +const toDateObject = (date: string | Date): Date => + typeof date === 'string' ? new Date(date) : date; + /** * Custom hook for localized date formatting with automatic fallback - * @param options - Intl.DateTimeFormat options - * @param fallbackOptions - Fallback options if normalized locale fails (defaults to en-US with same options) + * @param options - Configuration options for the date formatter * @returns Object containing formatDate function and dateFormatter instance */ export const useLocalizedDateFormatter = ( - options: DateFormatterOptions = { dateStyle: 'medium' }, - fallbackOptions?: DateFormatterOptions + options: UseLocalizedDateFormatterOptions = {} ): UseLocalizedDateFormatterReturn => { + const { + dateOptions = { dateStyle: 'medium' }, + fallbackOptions, + nullFallback = '—', + } = options; + const { i18n } = useTranslation(); const normalizedLocale = useMemo( @@ -36,38 +61,37 @@ export const useLocalizedDateFormatter = ( ); const fallbackDateFormatter = useMemo( - () => - new Intl.DateTimeFormat('en-US', fallbackOptions ?? options), - [fallbackOptions, options] + () => new Intl.DateTimeFormat('en-US', fallbackOptions ?? dateOptions), + [fallbackOptions, dateOptions] ); const dateFormatter = useMemo(() => { try { - return new Intl.DateTimeFormat(normalizedLocale, options); + return new Intl.DateTimeFormat(normalizedLocale, dateOptions); } catch (error) { console.error('Unsupported locale for date formatting', error); return fallbackDateFormatter; } - }, [normalizedLocale, options, fallbackDateFormatter]); + }, [normalizedLocale, dateOptions, fallbackDateFormatter]); const formatDate = useCallback( (date: string | Date | null | undefined) => { - if (!date) return '—'; + if (!date) return nullFallback; try { - const dateObject = typeof date === 'string' ? new Date(date) : date; + const dateObject = toDateObject(date); return dateFormatter.format(dateObject); } catch (error) { console.error('Error formatting date', error); try { - const dateObject = typeof date === 'string' ? new Date(date) : date; + const dateObject = toDateObject(date); return fallbackDateFormatter.format(dateObject); } catch { - return '—'; + return nullFallback; } } }, - [dateFormatter, fallbackDateFormatter] + [dateFormatter, fallbackDateFormatter, nullFallback] ); return { formatDate, dateFormatter }; diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx index 1326852..443a3bb 100644 --- a/src/pages/Blog.tsx +++ b/src/pages/Blog.tsx @@ -87,7 +87,9 @@ const Blog = () => { const { t } = useTranslation(); const [page, setPage] = useState(1); - const { formatDate } = useLocalizedDateFormatter({ dateStyle: 'medium' }); + const { formatDate } = useLocalizedDateFormatter({ + dateOptions: { dateStyle: 'medium' }, + }); const categories = useMemo( () => BLOG_CATEGORY_KEYS.map((key) => t(key)), diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index c4f3f04..9184e11 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -150,8 +150,10 @@ const Dashboard = () => { }, [loadDashboard]); const { formatDate } = useLocalizedDateFormatter({ - dateStyle: 'short', - timeStyle: 'short', + dateOptions: { + dateStyle: 'short', + timeStyle: 'short', + }, }); const handleRefresh = () => { diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index eabae28..92c788f 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -42,9 +42,11 @@ const Projects = () => { const memoizedFallbackSolutions = useMemo(() => getFallbackSolutions(), []); const { formatDate } = useLocalizedDateFormatter({ - year: 'numeric', - month: 'short', - day: 'numeric', + dateOptions: { + year: 'numeric', + month: 'short', + day: 'numeric', + }, }); const { diff --git a/src/pages/blog/[slug].tsx b/src/pages/blog/[slug].tsx index f7bb750..9c4c274 100644 --- a/src/pages/blog/[slug].tsx +++ b/src/pages/blog/[slug].tsx @@ -229,7 +229,9 @@ const BlogPostPage = () => { const { slug } = useParams<{ slug: string }>(); const { t } = useTranslation(); - const { formatDate } = useLocalizedDateFormatter({ dateStyle: 'long' }); + const { formatDate } = useLocalizedDateFormatter({ + dateOptions: { dateStyle: 'long' }, + }); const { data: post, From 1bf10d22ad3efd0e50f1c605779b86185c135d07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:17:26 +0000 Subject: [PATCH 4/4] Improve date validation and error handling in useLocalizedDateFormatter Co-authored-by: marcelo-m7 <117441129+marcelo-m7@users.noreply.github.com> --- src/hooks/useLocalizedDateFormatter.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/hooks/useLocalizedDateFormatter.ts b/src/hooks/useLocalizedDateFormatter.ts index 8d292f7..48432f6 100644 --- a/src/hooks/useLocalizedDateFormatter.ts +++ b/src/hooks/useLocalizedDateFormatter.ts @@ -35,9 +35,18 @@ interface UseLocalizedDateFormatterReturn { /** * Helper function to convert date input to Date object + * @throws Error if date is invalid */ -const toDateObject = (date: string | Date): Date => - typeof date === 'string' ? new Date(date) : date; +const toDateObject = (date: string | Date): Date => { + const dateObj = typeof date === 'string' ? new Date(date) : date; + + // Check if date is valid + if (Number.isNaN(dateObj.getTime())) { + throw new Error('Invalid date'); + } + + return dateObj; +}; /** * Custom hook for localized date formatting with automatic fallback @@ -80,15 +89,16 @@ export const useLocalizedDateFormatter = ( try { const dateObject = toDateObject(date); - return dateFormatter.format(dateObject); - } catch (error) { - console.error('Error formatting date', error); + try { - const dateObject = toDateObject(date); + return dateFormatter.format(dateObject); + } catch (formattingError) { + console.error('Error formatting date with primary formatter', formattingError); return fallbackDateFormatter.format(dateObject); - } catch { - return nullFallback; } + } catch (dateError) { + console.error('Invalid date provided', dateError); + return nullFallback; } }, [dateFormatter, fallbackDateFormatter, nullFallback]