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
49 changes: 26 additions & 23 deletions apps/web/src/routes/_view/blog/$slug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,30 +65,31 @@ function Component() {
const hasCoverImage = !coverImageError;

return (
<div className="min-h-screen bg-linear-to-b from-white via-stone-50/20 to-white">
<div className="max-w-6xl mx-auto border-x border-neutral-100">
<div
className="bg-linear-to-b from-white via-stone-50/20 to-white"
style={{ backgroundImage: "url(/patterns/dots.svg)" }}
>
<div className="min-h-screen max-w-6xl mx-auto border-x border-neutral-100 bg-white">
<MobileHeader />

<div className="px-4 sm:px-6 lg:px-8 py-8 lg:py-16">
<div className="sm:grid sm:grid-cols-12 sm:gap-8">
<TableOfContents toc={article.toc} />
<div className="sm:grid sm:grid-cols-12 sm:gap-8">
<TableOfContents toc={article.toc} />

<main className="sm:col-span-8 lg:col-span-6">
<ArticleHeader article={article} />
<CoverImage
article={article}
hasCoverImage={hasCoverImage}
coverImageLoaded={coverImageLoaded}
onLoad={() => setCoverImageLoaded(true)}
onError={() => setCoverImageError(true)}
/>
<ArticleContent article={article} />
<RelatedArticlesMobile relatedArticles={relatedArticles} />
<ArticleFooter />
</main>
<main className="sm:col-span-8 lg:col-span-6 py-4">
<CoverImage
article={article}
hasCoverImage={hasCoverImage}
coverImageLoaded={coverImageLoaded}
onLoad={() => setCoverImageLoaded(true)}
onError={() => setCoverImageError(true)}
/>
<ArticleHeader article={article} />
<ArticleContent article={article} />
<RelatedArticlesMobile relatedArticles={relatedArticles} />
<ArticleFooter />
</main>

<RightSidebar relatedArticles={relatedArticles} />
</div>
<RightSidebar relatedArticles={relatedArticles} />
</div>

<MobileCTA />
Expand Down Expand Up @@ -120,7 +121,7 @@ function TableOfContents({
}) {
return (
<aside className="hidden lg:block lg:col-span-3">
<div className="sticky top-24 max-h-[calc(100vh-6rem)] overflow-y-auto">
<div className="sticky top-[65px] max-h-[calc(100vh-65px)] overflow-y-auto p-4">
<Link
to="/blog"
className="inline-flex items-center gap-2 text-sm text-neutral-600 hover:text-stone-600 transition-colors mb-8"
Expand Down Expand Up @@ -216,9 +217,11 @@ function CoverImage({
<img
src={article.coverImage}
alt={article.title}
width={1200}
height={630}
className={cn(
[
"w-full aspect-video object-cover rounded-none sm:rounded-sm border-y sm:border border-neutral-200 transition-opacity duration-300",
"w-full aspect-40/21 object-cover rounded-none sm:rounded-sm border-y sm:border border-neutral-200 transition-opacity duration-300",
coverImageLoaded ? "opacity-100" : "opacity-0",
],
)}
Expand Down Expand Up @@ -275,7 +278,7 @@ function ArticleFooter() {
function RightSidebar({ relatedArticles }: { relatedArticles: any[] }) {
return (
<aside className="hidden sm:block sm:col-span-4 lg:col-span-3">
<div className="sticky top-24 space-y-8">
<div className="sticky top-[65px] space-y-8 p-4">
{relatedArticles.length > 0 && (
<div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-4">
Expand Down
84 changes: 24 additions & 60 deletions apps/web/src/routes/_view/blog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ function Component() {
});

const featuredArticles = sortedArticles.filter((a) => a.featured);
const regularArticles = sortedArticles.filter((a) => !a.featured);

return (
<div
Expand All @@ -37,10 +36,7 @@ function Component() {
<div className="px-4 sm:px-6 lg:px-8 py-16">
<Header />
<FeaturedSection articles={featuredArticles} />
<AllArticlesSection
featuredArticles={featuredArticles}
regularArticles={regularArticles}
/>
<AllArticlesSection articles={sortedArticles} />
</div>
</div>
</div>
Expand All @@ -66,37 +62,27 @@ function FeaturedSection({ articles }: { articles: Article[] }) {
return (
<section className="mb-20">
<SectionHeader title="Featured" />
<div className="grid gap-8 md:grid-cols-2">
{articles.slice(0, 2).map((article) => <FeaturedCard key={article._meta.filePath} article={article} />)}
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{articles.slice(0, 3).map((article) => <FeaturedCard key={article._meta.filePath} article={article} />)}
</div>
</section>
);
}

function AllArticlesSection({
featuredArticles,
regularArticles,
}: {
featuredArticles: Article[];
regularArticles: Article[];
}) {
if (regularArticles.length === 0 && featuredArticles.length === 0) {
function AllArticlesSection({ articles }: { articles: Article[] }) {
if (articles.length === 0) {
return (
<div className="text-center py-16">
<p className="text-neutral-500">No articles yet. Check back soon!</p>
</div>
);
}

if (regularArticles.length === 0) {
return null;
}

return (
<section>
<SectionHeader title="All Articles" />
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{regularArticles.map((article) => <ArticleCard key={article._meta.filePath} article={article} />)}
<SectionHeader title="All" />
<div className="divide-y divide-neutral-100">
{articles.map((article) => <ArticleListItem key={article._meta.filePath} article={article} />)}
</div>
</section>
);
Expand Down Expand Up @@ -153,47 +139,25 @@ function FeaturedCard({ article }: { article: Article }) {
);
}

function ArticleCard({ article }: { article: Article }) {
const [coverImageError, setCoverImageError] = useState(false);
const [coverImageLoaded, setCoverImageLoaded] = useState(false);
const hasCoverImage = !coverImageError;
function ArticleListItem({ article }: { article: Article }) {
const displayDate = article.updated || article.created;

return (
<Link to="/blog/$slug" params={{ slug: article.slug }} className="group block h-full">
<article className="h-full border border-neutral-100 rounded-sm overflow-hidden bg-white hover:shadow-lg transition-all duration-300 flex flex-col">
{hasCoverImage && (
<ArticleImage
src={article.coverImage}
alt={article.title}
isLoaded={coverImageLoaded}
onLoad={() => setCoverImageLoaded(true)}
onError={() => setCoverImageError(true)}
loading="lazy"
/>
)}

<div className="p-6 flex flex-col flex-1">
<h3 className="text-xl font-serif text-stone-600 mb-2 group-hover:text-stone-800 transition-colors line-clamp-2">
<Link to="/blog/$slug" params={{ slug: article.slug }} className="group block">
<article className="py-4 hover:bg-stone-50/50 transition-colors duration-200">
<div className="flex items-center gap-3">
<span className="text-base font-serif text-stone-600 group-hover:text-stone-800 transition-colors">
{article.title}
</h3>

<p className="text-sm text-neutral-600 leading-relaxed mb-4 line-clamp-2 flex-1">
{article.summary}
</p>

<div className="flex items-center justify-between gap-4 pt-4 border-t border-neutral-100">
<time dateTime={displayDate} className="text-xs text-neutral-500">
{new Date(displayDate).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})}
</time>

<span className="text-xs text-neutral-500 group-hover:text-stone-600 transition-colors font-medium">
Read →
</span>
</div>
</span>
<span className="text-sm text-neutral-500">by {article.author}</span>
<div className="h-px flex-1 bg-neutral-200" />
<time dateTime={displayDate} className="text-sm text-neutral-500 flex-shrink-0">
{new Date(displayDate).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
</time>
</div>
</article>
</Link>
Expand All @@ -220,7 +184,7 @@ function ArticleImage({
}

return (
<div className="aspect-video overflow-hidden border-b border-neutral-100 bg-stone-50">
<div className="aspect-40/21 overflow-hidden border-b border-neutral-100 bg-stone-50">
<img
src={src}
alt={alt}
Expand Down
Loading