diff --git a/src/components/PageBreadcrumb.tsx b/src/components/PageBreadcrumb.tsx
new file mode 100644
index 0000000..bc9e455
--- /dev/null
+++ b/src/components/PageBreadcrumb.tsx
@@ -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 (
+
+
+
+
+ {t('navigation.home')}
+
+
+ {segments.map((segment, index) => {
+ const isLast = index === segments.length - 1;
+ return (
+
+
+
+ {isLast || !segment.href ? (
+ {segment.label}
+ ) : (
+
+ {segment.label}
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default PageBreadcrumb;
diff --git a/src/components/blog/CommentsSection.tsx b/src/components/blog/CommentsSection.tsx
index bd3aafe..e52f737 100644
--- a/src/components/blog/CommentsSection.tsx
+++ b/src/components/blog/CommentsSection.tsx
@@ -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;
@@ -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,
diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts
index b4013a5..df4866a 100644
--- a/src/lib/i18n.ts
+++ b/src/lib/i18n.ts
@@ -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);
+ }
+};
+
diff --git a/src/lib/queryOptions.ts b/src/lib/queryOptions.ts
new file mode 100644
index 0000000..a212f31
--- /dev/null
+++ b/src/lib/queryOptions.ts
@@ -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;
diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx
index 287c7e5..ba0032f 100644
--- a/src/pages/Blog.tsx
+++ b/src/pages/Blog.tsx
@@ -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,
@@ -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 { getNormalizedLocale, createDateFormatter } from '@/lib/i18n';
import type { Database } from '@/integrations/supabase/types';
const FALLBACK_IMAGE =
@@ -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(
() => [
@@ -239,19 +224,7 @@ const Blog = () => {
ogImage="/placeholder.svg"
/>
-
-
-
-
- {t('navigation.home')}
-
-
-
-
- {t('navigation.blog')}
-
-
-
+
diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx
index 6a1ffd6..94682fe 100644
--- a/src/pages/Contact.tsx
+++ b/src/pages/Contact.tsx
@@ -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',
@@ -157,19 +150,9 @@ const Contact = () => {
ogImage="/placeholder.svg"
/>
-
-
-
-
- {t('navigation.home')}
-
-
-
-
- {t('navigation.contact')}
-
-
-
+
@@ -204,19 +187,9 @@ const Contact = () => {
ogImage="/placeholder.svg"
/>
-
-
-
-
- {t('navigation.home')}
-
-
-
-
- {t('navigation.contact')}
-
-
-
+
{/* Hero Section */}
diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx
index 3d210d2..8f638db 100644
--- a/src/pages/Projects.tsx
+++ b/src/pages/Projects.tsx
@@ -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';
@@ -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';
@@ -79,9 +73,7 @@ const Projects = () => {
} = useQuery({
queryKey: ['solutions'],
queryFn: fetchSupabaseSolutions,
- staleTime: 1000 * 60 * 10,
- retry: 1,
- refetchOnWindowFocus: false,
+ ...LONG_STALE_QUERY_OPTIONS,
});
// Removed duplicate githubSolutions useQuery block
@@ -127,9 +119,7 @@ const Projects = () => {
mapGitHubRepoToContent(repository, index)
);
},
- staleTime: 1000 * 60 * 10,
- retry: 1,
- refetchOnWindowFocus: false,
+ ...LONG_STALE_QUERY_OPTIONS,
});
const combinedSolutions = useMemo(() => {
@@ -232,19 +222,9 @@ const Projects = () => {
ogImage="/placeholder.svg"
/>
-
-
-
-
- {t('navigation.home')}
-
-
-
-
- {t('navigation.projects')}
-
-
-
+
diff --git a/src/pages/Solutions.tsx b/src/pages/Solutions.tsx
index a1c8094..79f60e7 100644
--- a/src/pages/Solutions.tsx
+++ b/src/pages/Solutions.tsx
@@ -4,15 +4,8 @@ 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 { Link } from 'react-router-dom';
-import {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from '@/components/ui/breadcrumb';
import { ArrowRight, CheckCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { supabase } from '@/integrations/supabase';
@@ -20,6 +13,7 @@ import {
getFallbackSolutions,
mapSupabaseSolutionToContent,
} from '@/lib/solutions';
+import { LONG_STALE_QUERY_OPTIONS } from '@/lib/queryOptions';
import type { SolutionContent } from '@/types/solutions';
@@ -53,9 +47,7 @@ const Solutions = () => {
);
},
- staleTime: 1000 * 60 * 10,
- retry: 1,
- refetchOnWindowFocus: false,
+ ...LONG_STALE_QUERY_OPTIONS,
});
const displaySolutions =
@@ -105,19 +97,9 @@ const Solutions = () => {
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 a8c04ca..6259f4e 100644
--- a/src/pages/blog/[slug].tsx
+++ b/src/pages/blog/[slug].tsx
@@ -5,19 +5,12 @@ import { useTranslation } from 'react-i18next';
import { ArrowLeft, Clock, User } from 'lucide-react';
import Layout from '@/components/Layout';
import Meta from '@/components/Meta';
+import PageBreadcrumb from '@/components/PageBreadcrumb';
import NewsletterSection from '@/components/NewsletterSection';
import CommentsSection from '@/components/blog/CommentsSection';
import { supabase } from '@/integrations/supabase';
-import { getNormalizedLocale } from '@/lib/i18n';
+import { getNormalizedLocale, createDateFormatter } from '@/lib/i18n';
import type { Database } from '@/integrations/supabase/types';
-import {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from '@/components/ui/breadcrumb';
type BlogPost = Database['public']['Tables']['blog_posts']['Row'];
@@ -218,16 +211,10 @@ const BlogPostPage = () => {
[i18n.language]
);
- 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 dateFormatter = useMemo(
+ () => createDateFormatter(normalizedLocale, { dateStyle: 'long' }),
+ [normalizedLocale]
+ );
const {
data: post,
@@ -336,25 +323,12 @@ const BlogPostPage = () => {
ogImage={metaImage}
/>
-
-
-
-
- {t('navigation.home')}
-
-
-
-
-
- {t('navigation.blog')}
-
-
-
-
- {post.title}
-
-
-
+
diff --git a/src/pages/solutions/[slug].tsx b/src/pages/solutions/[slug].tsx
index 01859ca..f33b38c 100644
--- a/src/pages/solutions/[slug].tsx
+++ b/src/pages/solutions/[slug].tsx
@@ -3,16 +3,9 @@ import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import Layout from '@/components/Layout';
import Meta from '@/components/Meta';
+import PageBreadcrumb from '@/components/PageBreadcrumb';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from '@/components/ui/breadcrumb';
import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { supabase } from '@/integrations/supabase';
@@ -23,6 +16,7 @@ import {
mapGitHubRepoToContent,
mapSupabaseSolutionToContent,
} from '@/lib/solutions';
+import { LONG_STALE_QUERY_OPTIONS } from '@/lib/queryOptions';
const getRepositoryUrl = (repositorySlug: string) =>
`https://api.github.com/repos/Monynha-Softwares/${encodeURIComponent(
@@ -87,10 +81,8 @@ const SolutionDetail = () => {
return mapGitHubRepoToContent(repository, 0);
},
- staleTime: 1000 * 60 * 10,
- retry: 1,
+ ...LONG_STALE_QUERY_OPTIONS,
keepPreviousData: true,
- refetchOnWindowFocus: false,
});
const displaySolution = solution ?? fallbackSolution ?? null;
@@ -164,25 +156,12 @@ const SolutionDetail = () => {
ogImage={displaySolution.imageUrl ?? '/placeholder.svg'}
/>
-
-
-
-
- {t('navigation.home')}
-
-
-
-
-
- {t('navigation.solutions')}
-
-
-
-
- {displaySolution.title}
-
-
-
+