Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/components/PageBreadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={className}>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/">{t('navigation.home')}</Link>
</BreadcrumbLink>
</BreadcrumbItem>

{items.map((item, index) => (
<React.Fragment key={index}>
<BreadcrumbSeparator />
<BreadcrumbItem>
{item.path ? (
<BreadcrumbLink asChild>
<Link to={item.path}>{item.label}</Link>
</BreadcrumbLink>
) : (
<BreadcrumbPage>{item.label}</BreadcrumbPage>
)}
</BreadcrumbItem>
</React.Fragment>
))}

<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{t(currentPage)}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
);
};
33 changes: 9 additions & 24 deletions src/components/blog/CommentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<Card
Expand Down
108 changes: 108 additions & 0 deletions src/hooks/useLocalizedDateFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { getNormalizedLocale } from '@/lib/i18n';

type DateStyle = 'short' | 'medium' | 'long' | 'full';
type TimeStyle = 'short' | 'medium' | 'long' | 'full';

interface DateFormatterOptions {
dateStyle?: DateStyle;
timeStyle?: TimeStyle;
year?: 'numeric' | '2-digit';
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
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
* @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 };
};
55 changes: 10 additions & 45 deletions src/pages/Blog.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo } from 'react';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore useCallback import in Blog page

The top-level React import drops useCallback, but handlePageChange still calls useCallback later in this file. This will cause a compile/runtime failure (useCallback is not defined) when building the blog page. Re‑add useCallback to the import list so the pagination handler continues to work.

Useful? React with 👍 / 👎.

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,
Expand All @@ -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,
Expand All @@ -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 =
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -240,21 +219,7 @@ const Blog = () => {
ogDescription={t('blog.description')}
ogImage="/placeholder.svg"
/>
<div className="max-w-7xl mx-auto px-4 pt-4">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/">{t('navigation.home')}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{t('navigation.blog')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<PageBreadcrumb currentPage="navigation.blog" />
<section className="py-24 bg-white transition-colors dark:bg-neutral-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
Expand Down
Loading
Loading