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..b202378 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,18 @@ 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({ + dateOptions: { + dateStyle: 'medium', + timeStyle: 'short', + }, + }); const { data: comments, @@ -249,9 +236,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; +} + +/** + * Helper function to convert date input to Date object + * @throws Error if date is invalid + */ +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 + * @param options - Configuration options for the date formatter + * @returns Object containing formatDate function and dateFormatter instance + */ +export const useLocalizedDateFormatter = ( + options: UseLocalizedDateFormatterOptions = {} +): UseLocalizedDateFormatterReturn => { + const { + dateOptions = { dateStyle: 'medium' }, + fallbackOptions, + nullFallback = '—', + } = options; + + const { i18n } = useTranslation(); + + const normalizedLocale = useMemo( + () => getNormalizedLocale(i18n.language), + [i18n.language] + ); + + const fallbackDateFormatter = useMemo( + () => new Intl.DateTimeFormat('en-US', fallbackOptions ?? dateOptions), + [fallbackOptions, dateOptions] + ); + + const dateFormatter = useMemo(() => { + try { + return new Intl.DateTimeFormat(normalizedLocale, dateOptions); + } catch (error) { + console.error('Unsupported locale for date formatting', error); + return fallbackDateFormatter; + } + }, [normalizedLocale, dateOptions, fallbackDateFormatter]); + + const formatDate = useCallback( + (date: string | Date | null | undefined) => { + if (!date) return nullFallback; + + try { + const dateObject = toDateObject(date); + + try { + return dateFormatter.format(dateObject); + } catch (formattingError) { + console.error('Error formatting date with primary formatter', formattingError); + return fallbackDateFormatter.format(dateObject); + } + } catch (dateError) { + console.error('Invalid date provided', dateError); + return nullFallback; + } + }, + [dateFormatter, fallbackDateFormatter, nullFallback] + ); + + return { formatDate, dateFormatter }; +}; diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx index 76c4605..443a3bb 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,12 @@ 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({ + dateOptions: { dateStyle: 'medium' }, + }); const categories = useMemo( () => BLOG_CATEGORY_KEYS.map((key) => t(key)), @@ -160,12 +139,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 +219,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..9184e11 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,12 @@ 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)); - } + const { formatDate } = useLocalizedDateFormatter({ + dateOptions: { + dateStyle: 'short', + timeStyle: 'short', }, - [dateTimeFormatter, fallbackDateFormatter] - ); + }); const handleRefresh = () => { void loadDashboard(); diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 1c3de55..92c788f 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,18 @@ 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)); - } + const { formatDate } = useLocalizedDateFormatter({ + dateOptions: { + year: 'numeric', + month: 'short', + day: 'numeric', }, - [dateFormatter, fallbackDateFormatter] - ); + }); const { data: repositories = [], @@ -251,21 +212,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..9c4c274 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,11 @@ 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({ + dateOptions: { dateStyle: 'long' }, + }); const { data: post, @@ -279,9 +267,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