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
58 changes: 58 additions & 0 deletions src/components/PageBreadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';

export interface BreadcrumbSegment {
label: string;
href?: string;
}

interface PageBreadcrumbProps {
segments: BreadcrumbSegment[];
}

/**
* Reusable breadcrumb navigation component.
* Always starts with a "Home" link and then renders the provided segments.
*/
const PageBreadcrumb = ({ segments }: PageBreadcrumbProps) => {
const { t } = useTranslation();

return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/">{t('navigation.home')}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{segments.map((segment, index) => {
const isLast = index === segments.length - 1;
return (
<div key={index} className="contents">
<BreadcrumbSeparator />
<BreadcrumbItem>
{isLast || !segment.href ? (
<BreadcrumbPage>{segment.label}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link to={segment.href}>{segment.label}</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</div>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
};

export default PageBreadcrumb;
20 changes: 7 additions & 13 deletions src/components/blog/CommentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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';
import { getNormalizedLocale, createDateFormatter } from '@/lib/i18n';

interface CommentsSectionProps {
postId: string;
Expand Down Expand Up @@ -52,20 +52,14 @@ const CommentsSection = ({ postId }: CommentsSectionProps) => {
[i18n.language]
);

const dateFormatter = useMemo(() => {
try {
return new Intl.DateTimeFormat(normalizedLocale, {
const dateFormatter = useMemo(
() =>
createDateFormatter(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]);
}),
[normalizedLocale]
);

const {
data: comments,
Expand Down
17 changes: 17 additions & 0 deletions src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,20 @@ export const getNormalizedLocale = (language: string | undefined | null) => {
return language;
}
};

/**
* Creates a date formatter with fallback to en-US if the locale is not supported.
* This utility centralizes date formatting logic used across the application.
*/
export const createDateFormatter = (
locale: string,
options: Intl.DateTimeFormatOptions
): Intl.DateTimeFormat => {
try {
return new Intl.DateTimeFormat(locale, options);
} catch (error) {
console.error(`Unsupported locale "${locale}" for date formatting`, error);
return new Intl.DateTimeFormat('en-US', options);
}
};

16 changes: 16 additions & 0 deletions src/lib/queryOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Common React Query configuration options used across the application.
* Centralizes query behavior to ensure consistency.
*/

/**
* Standard query options for data that doesn't change frequently.
* - staleTime: 10 minutes - data stays fresh for this duration
* - retry: 1 - only retry failed queries once
* - refetchOnWindowFocus: false - don't refetch when window regains focus
*/
export const LONG_STALE_QUERY_OPTIONS = {
staleTime: 1000 * 60 * 10, // 10 minutes
retry: 1,
refetchOnWindowFocus: false,
} as const;
41 changes: 7 additions & 34 deletions src/pages/Blog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import Layout from '@/components/Layout';
import Meta from '@/components/Meta';
import PageBreadcrumb from '@/components/PageBreadcrumb';
import NewsletterSection from '@/components/NewsletterSection';
import {
ArrowRight,
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 { getNormalizedLocale, createDateFormatter } from '@/lib/i18n';
import type { Database } from '@/integrations/supabase/types';

const FALLBACK_IMAGE =
Expand Down Expand Up @@ -90,18 +83,10 @@ const Blog = () => {
[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 dateFormatter = useMemo(
() => createDateFormatter(normalizedLocale, { dateStyle: 'medium' }),
[normalizedLocale]
);

const categories = useMemo(
() => [
Expand Down Expand Up @@ -239,19 +224,7 @@ const Blog = () => {
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>
<PageBreadcrumb segments={[{ label: t('navigation.blog') }]} />
</div>
<section className="py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
Expand Down
41 changes: 7 additions & 34 deletions src/pages/Contact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,13 @@ 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, Phone, MapPin, Send, CheckCircle } from 'lucide-react';
import { supabase } from '@/integrations/supabase';
import { useToast } from '@/hooks/use-toast';
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';

const projectTypes = [
'Custom AI Assistant',
Expand Down Expand Up @@ -157,19 +150,9 @@ const Contact = () => {
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.contact')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<PageBreadcrumb
segments={[{ label: t('navigation.contact') }]}
/>
</div>
<section className="py-24 bg-white min-h-screen flex items-center">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
Expand Down Expand Up @@ -204,19 +187,9 @@ const Contact = () => {
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.contact')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<PageBreadcrumb
segments={[{ label: t('navigation.contact') }]}
/>
</div>
{/* Hero Section */}
<section className="py-24 bg-white">
Expand Down
34 changes: 7 additions & 27 deletions src/pages/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,10 @@ import {
} from 'lucide-react';
import Layout from '../components/Layout';
import Meta from '@/components/Meta';
import PageBreadcrumb from '@/components/PageBreadcrumb';
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 { supabase } from '@/integrations/supabase';
import { useTranslation, Trans } from 'react-i18next';
import { Link } from 'react-router-dom';
Expand All @@ -29,6 +22,7 @@ import {
getFallbackSolutions,
mapGitHubRepoToContent,
} from '@/lib/solutions';
import { LONG_STALE_QUERY_OPTIONS } from '@/lib/queryOptions';
import type { GitHubRepository } from '@/lib/solutions';
import type { SolutionContent } from '@/types/solutions';

Expand Down Expand Up @@ -79,9 +73,7 @@ const Projects = () => {
} = useQuery<SolutionContent[]>({
queryKey: ['solutions'],
queryFn: fetchSupabaseSolutions,
staleTime: 1000 * 60 * 10,
retry: 1,
refetchOnWindowFocus: false,
...LONG_STALE_QUERY_OPTIONS,
});

// Removed duplicate githubSolutions useQuery block
Expand Down Expand Up @@ -127,9 +119,7 @@ const Projects = () => {
mapGitHubRepoToContent(repository, index)
);
},
staleTime: 1000 * 60 * 10,
retry: 1,
refetchOnWindowFocus: false,
...LONG_STALE_QUERY_OPTIONS,
});

const combinedSolutions = useMemo(() => {
Expand Down Expand Up @@ -232,19 +222,9 @@ const Projects = () => {
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.projects')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<PageBreadcrumb
segments={[{ label: t('navigation.projects') }]}
/>
</div>

<div className="container mx-auto px-4 py-16">
Expand Down
Loading
Loading