feat: add feature UI components with useQuery hooks
- navigation: SiteHeader component + useHeader hook - blog: ArticleCard, ArticleList, ArticleDetail + useArticleList, useArticleBySlug hooks - marketing-pages: PageHero, PageContent + usePageBySlug, useSiteSettings hooks - App pages now thin server wrappers that prefetch + hydrate; feature components own their data fetching via useSuspenseQuery
This commit is contained in:
23
packages/blog/src/ui/components/article-card.tsx
Normal file
23
packages/blog/src/ui/components/article-card.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export type ArticleCardProps = {
|
||||
article: Article;
|
||||
};
|
||||
|
||||
export function ArticleCard({ article }: ArticleCardProps) {
|
||||
return (
|
||||
<article className="rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/50">
|
||||
<a href={`/blog/${article.slug}`}>
|
||||
<h3 className="text-lg font-semibold text-card-foreground">
|
||||
{article.title}
|
||||
</h3>
|
||||
</a>
|
||||
<time
|
||||
className="text-sm text-muted-foreground"
|
||||
dateTime={article.createdAt.toISOString()}
|
||||
>
|
||||
{article.createdAt.toLocaleDateString()}
|
||||
</time>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
34
packages/blog/src/ui/components/article-detail.tsx
Normal file
34
packages/blog/src/ui/components/article-detail.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useArticleBySlug } from "../hooks/use-article-by-slug";
|
||||
|
||||
export type ArticleDetailProps = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export function ArticleDetail({ slug }: ArticleDetailProps) {
|
||||
const { data: article } = useArticleBySlug(slug);
|
||||
|
||||
if (!article) return null;
|
||||
|
||||
return (
|
||||
<article className="mx-auto max-w-3xl">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">{article.title}</h1>
|
||||
{article.createdAt ? (
|
||||
<time
|
||||
className="mt-2 block text-sm text-muted-foreground"
|
||||
dateTime={article.createdAt.toISOString()}
|
||||
>
|
||||
{article.createdAt.toLocaleDateString()}
|
||||
</time>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="prose text-foreground">
|
||||
<pre className="whitespace-pre-wrap text-sm">
|
||||
{JSON.stringify(article.content, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
20
packages/blog/src/ui/components/article-list.tsx
Normal file
20
packages/blog/src/ui/components/article-list.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useArticleList } from "../hooks/use-article-list";
|
||||
import { ArticleCard } from "./article-card";
|
||||
|
||||
export function ArticleList() {
|
||||
const { data: articles } = useArticleList();
|
||||
|
||||
if (articles.length === 0) {
|
||||
return <p className="text-muted-foreground">No published articles yet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
{articles.map((article) => (
|
||||
<ArticleCard key={article.id} article={article} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
packages/blog/src/ui/hooks/use-article-by-slug.ts
Normal file
12
packages/blog/src/ui/hooks/use-article-by-slug.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export function useArticleBySlug(slug: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.blog.articleBySlug.queryOptions({ slug })) as {
|
||||
data: Article | null;
|
||||
};
|
||||
}
|
||||
18
packages/blog/src/ui/hooks/use-article-list.ts
Normal file
18
packages/blog/src/ui/hooks/use-article-list.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export function useArticleList(options?: {
|
||||
status?: "draft" | "published";
|
||||
limit?: number;
|
||||
}) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(
|
||||
trpc.blog.listArticles.queryOptions({
|
||||
status: options?.status ?? "published",
|
||||
limit: options?.limit ?? 20,
|
||||
}),
|
||||
) as { data: Article[] };
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
export { articleBySlugQuery, listArticlesQuery } from "./query";
|
||||
export { useArticleList } from "./hooks/use-article-list";
|
||||
export { useArticleBySlug } from "./hooks/use-article-by-slug";
|
||||
export { ArticleCard, type ArticleCardProps } from "./components/article-card";
|
||||
export { ArticleList } from "./components/article-list";
|
||||
export {
|
||||
ArticleDetail,
|
||||
type ArticleDetailProps,
|
||||
} from "./components/article-detail";
|
||||
|
||||
Reference in New Issue
Block a user