Files
sase.tr/apps/web/src/components/blog-content.tsx
Semih Yesilyurt 3c9245bd8b
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
feat(web): keep sidebar on blog & contact for logged-in users
Sidebar linked /blog and /contact pointed at public marketing pages, so
clicking them dropped the user out of the dashboard shell. Extract page
content into shared components (blog-content, contact-content) and add
dashboard-wrapped routes /dashboard/blog, /dashboard/blog/$slug and
/dashboard/contact; sidebar now links to those. Public SEO pages stay
unchanged and remain the canonical URLs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:25:14 +03:00

158 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useBlogPost, useBlogPosts } from "@/hooks/use-blog";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
import { Link } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
// Blog content is rendered in two shells: the public marketing pages (/blog)
// and the dashboard-wrapped pages (/dashboard/blog) so logged-in users keep
// the sidebar. Only the internal link targets differ; the canonical URL always
// points at the public page.
type BlogListPath = "/blog" | "/dashboard/blog";
type BlogPostPath = "/blog/$slug" | "/dashboard/blog/$slug";
// ─── LIST ─────────────────────────────────────────────────────────────────────
export function BlogListContent({ postLinkTo }: { postLinkTo: BlogPostPath }) {
usePageMeta({
title: "Blog — Sase.tr | Şase & Yedek Parça Rehberi",
description: "Şase numarası okuma, OEM vs muadil parça, dijital dönüşüm ve daha fazlası.",
canonical: "https://sase.tr/blog",
});
// All posts come from the central Directus CMS through the API.
const { data: apiPosts, isLoading } = useBlogPosts();
const posts = (apiPosts ?? [])
.map((p) => ({
slug: p.slug,
title: p.title,
description: p.metaDescription ?? "",
date: p.publishedAt.slice(0, 10),
}))
.sort((a, b) => b.date.localeCompare(a.date));
return (
<>
<h1 className="text-4xl font-bold">Blog</h1>
<p className="mt-4 text-lg text-muted-foreground">
Yedek parça sektörü, araç bakımı ve Sase.tr hakkında güncel yazılar.
</p>
{isLoading && posts.length === 0 && (
<p className="mt-12 text-muted-foreground">Yükleniyor</p>
)}
<div className="mt-12 grid gap-6 sm:grid-cols-2">
{posts.map((post) => (
<Link key={post.slug} to={postLinkTo} params={{ slug: post.slug }}>
<Card className="h-full transition hover:border-foreground/20">
<CardHeader>
<CardDescription>{post.date}</CardDescription>
<CardTitle className="text-lg">{post.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{post.description}</p>
</CardContent>
</Card>
</Link>
))}
</div>
</>
);
}
// ─── POST ─────────────────────────────────────────────────────────────────────
// Posts live in the central Directus CMS and arrive as markdown via the API.
const mdComponents = {
h2: (props: React.ComponentProps<"h2">) => (
<h2 className="text-xl font-semibold text-foreground" {...props} />
),
h3: (props: React.ComponentProps<"h3">) => (
<h3 className="text-lg font-semibold text-foreground" {...props} />
),
ul: (props: React.ComponentProps<"ul">) => <ul className="list-disc space-y-2 pl-6" {...props} />,
ol: (props: React.ComponentProps<"ol">) => (
<ol className="list-decimal space-y-2 pl-6" {...props} />
),
strong: (props: React.ComponentProps<"strong">) => (
<strong className="text-foreground" {...props} />
),
a: (props: React.ComponentProps<"a">) => <a className="text-foreground underline" {...props} />,
};
function MarkdownBody({ markdown }: { markdown: string }) {
return (
<div className="space-y-6 leading-relaxed text-muted-foreground">
<ReactMarkdown remarkPlugins={[remarkGfm]} components={mdComponents}>
{markdown}
</ReactMarkdown>
</div>
);
}
export function BlogPostContent({ slug, listLinkTo }: { slug: string; listLinkTo: BlogListPath }) {
const { data: post, isLoading, isError } = useBlogPost(slug);
const title = post?.title ?? "Blog";
const description = post?.metaDescription ?? "";
const date = post?.publishedAt?.slice(0, 10) ?? "";
usePageMeta({
title: `${title} | Sase.tr Blog`,
description,
canonical: `https://sase.tr/blog/${slug}`,
});
let body: React.ReactNode;
if (post) body = <MarkdownBody markdown={post.bodyMarkdown} />;
else if (isLoading) body = <p className="text-muted-foreground">Yükleniyor</p>;
else
body = (
<p className="text-muted-foreground">{isError ? "Yazı bulunamadı." : "Yazı yüklenemedi."}</p>
);
return (
<>
{/* Breadcrumb */}
<nav className="mb-8 flex items-center gap-1.5 text-sm text-muted-foreground">
<Link to={listLinkTo} className="transition hover:text-foreground">
Blog
</Link>
<ChevronRight className="size-3.5" />
<span className="text-foreground">{title}</span>
</nav>
<article>
{date && <time className="text-sm text-muted-foreground">{date}</time>}
<h1 className="mt-3 text-3xl font-bold leading-tight sm:text-4xl">{title}</h1>
{description && <p className="mt-4 text-lg text-muted-foreground">{description}</p>}
{post?.coverImage && (
<img
src={post.coverImage}
alt={title}
className="mt-8 w-full rounded-lg border object-cover"
/>
)}
<div className="mt-10">{body}</div>
</article>
{/* Back link */}
<div className="mt-16 border-t pt-8">
<Link
to={listLinkTo}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground transition hover:text-foreground"
>
Tüm yazılar
</Link>
</div>
</>
);
}