mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
Update: Blog page featured layout ui, mobile menu padding and font change
This commit is contained in:
89
src/components/BlogHeroCard.tsx
Normal file
89
src/components/BlogHeroCard.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { format, parseISO } from "date-fns";
|
||||
|
||||
interface BlogHeroCardProps {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
readTime?: string;
|
||||
image?: string;
|
||||
excerpt?: string;
|
||||
authorName?: string;
|
||||
authorImage?: string;
|
||||
}
|
||||
|
||||
// Hero card component for featured blog post on /blog page
|
||||
// Displays as a large card with image on left, content on right (like Giga.ai/news)
|
||||
export default function BlogHeroCard({
|
||||
slug,
|
||||
title,
|
||||
description,
|
||||
date,
|
||||
tags,
|
||||
readTime,
|
||||
image,
|
||||
excerpt,
|
||||
authorName,
|
||||
authorImage,
|
||||
}: BlogHeroCardProps) {
|
||||
return (
|
||||
<Link to={`/${slug}`} className="blog-hero-card">
|
||||
{/* Hero image on the left */}
|
||||
{image && (
|
||||
<div className="blog-hero-image-wrapper">
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="blog-hero-image"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content on the right */}
|
||||
<div className="blog-hero-content">
|
||||
{/* Tags displayed as labels */}
|
||||
{tags.length > 0 && (
|
||||
<div className="blog-hero-tags">
|
||||
{tags.slice(0, 2).map((tag) => (
|
||||
<span key={tag} className="blog-hero-tag">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<time className="blog-hero-date">
|
||||
{format(parseISO(date), "MMM d, yyyy").toUpperCase()}
|
||||
</time>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="blog-hero-title">{title}</h2>
|
||||
|
||||
{/* Description or excerpt */}
|
||||
<p className="blog-hero-excerpt">{excerpt || description}</p>
|
||||
|
||||
{/* Author info and read more */}
|
||||
<div className="blog-hero-footer">
|
||||
{authorName && (
|
||||
<div className="blog-hero-author">
|
||||
{authorImage && (
|
||||
<img
|
||||
src={authorImage}
|
||||
alt={authorName}
|
||||
className="blog-hero-author-image"
|
||||
/>
|
||||
)}
|
||||
<span className="blog-hero-author-name">{authorName}</span>
|
||||
</div>
|
||||
)}
|
||||
{readTime && <span className="blog-hero-read-time">{readTime}</span>}
|
||||
<span className="blog-hero-read-more">Read more</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -221,10 +221,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
</nav>
|
||||
</MobileMenu>
|
||||
|
||||
{/* Use wider layout for stats page, normal layout for other pages */}
|
||||
{/* Use wider layout for stats and blog pages, normal layout for other pages */}
|
||||
<main
|
||||
className={
|
||||
location.pathname === "/stats" ? "main-content-wide" : "main-content"
|
||||
location.pathname === "/stats" || location.pathname === "/blog"
|
||||
? "main-content-wide"
|
||||
: "main-content"
|
||||
}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -16,6 +16,8 @@ interface Post {
|
||||
interface PostListProps {
|
||||
posts: Post[];
|
||||
viewMode?: "list" | "cards";
|
||||
columns?: 2 | 3; // Number of columns for card view (default: 3)
|
||||
showExcerpts?: boolean; // Show excerpts in card view (default: true)
|
||||
}
|
||||
|
||||
// Group posts by year
|
||||
@@ -33,7 +35,12 @@ function groupByYear(posts: Post[]): Record<string, Post[]> {
|
||||
);
|
||||
}
|
||||
|
||||
export default function PostList({ posts, viewMode = "list" }: PostListProps) {
|
||||
export default function PostList({
|
||||
posts,
|
||||
viewMode = "list",
|
||||
columns = 3,
|
||||
showExcerpts = true,
|
||||
}: PostListProps) {
|
||||
// Sort posts by date descending
|
||||
const sortedPosts = [...posts].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
@@ -41,8 +48,11 @@ export default function PostList({ posts, viewMode = "list" }: PostListProps) {
|
||||
|
||||
// Card view: render all posts in a grid
|
||||
if (viewMode === "cards") {
|
||||
// Apply column class for 2 or 3 columns
|
||||
const cardGridClass =
|
||||
columns === 2 ? "post-cards post-cards-2col" : "post-cards";
|
||||
return (
|
||||
<div className="post-cards">
|
||||
<div className={cardGridClass}>
|
||||
{sortedPosts.map((post) => (
|
||||
<Link key={post._id} to={`/${post.slug}`} className="post-card">
|
||||
{/* Thumbnail image displayed as square using object-fit: cover */}
|
||||
@@ -58,7 +68,8 @@ export default function PostList({ posts, viewMode = "list" }: PostListProps) {
|
||||
)}
|
||||
<div className="post-card-content">
|
||||
<h3 className="post-card-title">{post.title}</h3>
|
||||
{(post.excerpt || post.description) && (
|
||||
{/* Only show excerpt if showExcerpts is true */}
|
||||
{showExcerpts && (post.excerpt || post.description) && (
|
||||
<p className="post-card-excerpt">
|
||||
{post.excerpt || post.description}
|
||||
</p>
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface FooterConfig {
|
||||
showOnHomepage: boolean; // Show footer on homepage
|
||||
showOnPosts: boolean; // Default: show footer on blog posts
|
||||
showOnPages: boolean; // Default: show footer on static pages
|
||||
showOnBlogPage: boolean; // Show footer on /blog page
|
||||
defaultContent?: string; // Default markdown content if no frontmatter footer field provided
|
||||
}
|
||||
|
||||
@@ -266,7 +267,7 @@ export const siteConfig: SiteConfig = {
|
||||
title: "Blog", // Page title
|
||||
description: "All posts from the blog, sorted by date.", // Optional description
|
||||
order: 2, // Nav order (lower = first, e.g., 0 = first, 5 = after pages with order 0-4)
|
||||
viewMode: "list", // Default view mode: "list" or "cards"
|
||||
viewMode: "cards", // Default view mode: "list" or "cards"
|
||||
showViewToggle: true, // Show toggle button to switch between list and card views
|
||||
},
|
||||
|
||||
@@ -337,6 +338,7 @@ export const siteConfig: SiteConfig = {
|
||||
showOnHomepage: true, // Show footer on homepage
|
||||
showOnPosts: true, // Default: show footer on blog posts (override with frontmatter)
|
||||
showOnPages: true, // Default: show footer on static pages (override with frontmatter)
|
||||
showOnBlogPage: true, // Show footer on /blog page
|
||||
// Default footer markdown (used when frontmatter footer field is not provided)
|
||||
defaultContent: `Built with [Convex](https://convex.dev) for real-time sync and deployed on [Netlify](https://netlify.com). Read the [project on GitHub](https://github.com/waynesutton/markdown-site) to fork and deploy your own. View [real-time site stats](/stats).
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import PostList from "../components/PostList";
|
||||
import BlogHeroCard from "../components/BlogHeroCard";
|
||||
import Footer from "../components/Footer";
|
||||
import siteConfig from "../config/siteConfig";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
@@ -10,14 +12,20 @@ import { ArrowLeft } from "lucide-react";
|
||||
const BLOG_VIEW_MODE_KEY = "blog-view-mode";
|
||||
|
||||
// Blog page component
|
||||
// Displays all published posts in a year-grouped list or card grid
|
||||
// Displays all published posts with featured blog posts layout:
|
||||
// 1. Hero: first blogFeatured post (large card)
|
||||
// 2. Featured row: remaining blogFeatured posts (2 columns)
|
||||
// 3. Regular posts: non-featured posts (3 columns)
|
||||
// Controlled by siteConfig.blogPage and siteConfig.postsDisplay settings
|
||||
export default function Blog() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Fetch published posts from Convex
|
||||
// Fetch all published posts from Convex
|
||||
const posts = useQuery(api.posts.getAllPosts);
|
||||
|
||||
// Fetch all blog featured posts for hero + featured row
|
||||
const blogFeaturedPosts = useQuery(api.posts.getBlogFeaturedPosts);
|
||||
|
||||
// State for view mode toggle (list or cards)
|
||||
const [viewMode, setViewMode] = useState<"list" | "cards">(
|
||||
siteConfig.blogPage.viewMode,
|
||||
@@ -41,8 +49,33 @@ export default function Blog() {
|
||||
// Check if posts should be shown on blog page
|
||||
const showPosts = siteConfig.postsDisplay.showOnBlogPage;
|
||||
|
||||
// Check if footer should be shown on blog page
|
||||
const showFooter =
|
||||
siteConfig.footer.enabled && siteConfig.footer.showOnBlogPage;
|
||||
|
||||
// Split featured posts: first one is hero, rest go to featured row
|
||||
const heroPost = blogFeaturedPosts && blogFeaturedPosts.length > 0 ? blogFeaturedPosts[0] : null;
|
||||
const featuredRowPosts = blogFeaturedPosts && blogFeaturedPosts.length > 1 ? blogFeaturedPosts.slice(1) : [];
|
||||
|
||||
// Get slugs of all featured posts for filtering
|
||||
const featuredSlugs = new Set(blogFeaturedPosts?.map((p) => p.slug) || []);
|
||||
|
||||
// Filter out all featured posts from regular posts list
|
||||
const regularPosts = posts?.filter((post) => !featuredSlugs.has(post.slug));
|
||||
|
||||
// Determine if we have featured content to show
|
||||
const hasFeaturedContent = heroPost !== null;
|
||||
|
||||
// Build CSS class for the blog page
|
||||
const blogPageClass = [
|
||||
"blog-page",
|
||||
viewMode === "cards" ? "blog-page-cards" : "blog-page-list",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div className="blog-page">
|
||||
<div className={blogPageClass}>
|
||||
{/* Navigation with back button */}
|
||||
<nav className="post-nav">
|
||||
<button onClick={() => navigate("/")} className="back-button">
|
||||
@@ -55,12 +88,12 @@ export default function Blog() {
|
||||
<header className="blog-header">
|
||||
<div className="blog-header-top">
|
||||
<div>
|
||||
<h1 className="blog-title">{siteConfig.blogPage.title}</h1>
|
||||
{siteConfig.blogPage.description && (
|
||||
<h1 className="blog-title">{siteConfig.blogPage.title}</h1>
|
||||
{siteConfig.blogPage.description && (
|
||||
<p className="blog-description">
|
||||
{siteConfig.blogPage.description}
|
||||
</p>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
{/* View toggle button */}
|
||||
{showPosts &&
|
||||
@@ -112,13 +145,50 @@ export default function Blog() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Blog posts section */}
|
||||
{/* Hero featured post section (only in cards view) */}
|
||||
{showPosts && hasFeaturedContent && viewMode === "cards" && heroPost && (
|
||||
<section className="blog-hero-section">
|
||||
<BlogHeroCard
|
||||
slug={heroPost.slug}
|
||||
title={heroPost.title}
|
||||
description={heroPost.description}
|
||||
date={heroPost.date}
|
||||
tags={heroPost.tags}
|
||||
readTime={heroPost.readTime}
|
||||
image={heroPost.image}
|
||||
excerpt={heroPost.excerpt}
|
||||
authorName={heroPost.authorName}
|
||||
authorImage={heroPost.authorImage}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Featured row: remaining featured posts in 2 columns (only in cards view) */}
|
||||
{showPosts && featuredRowPosts.length > 0 && viewMode === "cards" && (
|
||||
<section className="blog-featured-row">
|
||||
<PostList
|
||||
posts={featuredRowPosts}
|
||||
viewMode="cards"
|
||||
columns={2}
|
||||
showExcerpts={true}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Regular posts section: non-featured posts in 3 columns */}
|
||||
{showPosts && (
|
||||
<section className="blog-posts">
|
||||
{posts === undefined ? null : posts.length === 0 ? (
|
||||
<p className="no-posts">No posts yet. Check back soon!</p>
|
||||
{regularPosts === undefined ? null : regularPosts.length === 0 ? (
|
||||
!hasFeaturedContent && (
|
||||
<p className="no-posts">No posts yet. Check back soon!</p>
|
||||
)
|
||||
) : (
|
||||
<PostList posts={posts} viewMode={viewMode} />
|
||||
<PostList
|
||||
posts={regularPosts}
|
||||
viewMode={viewMode}
|
||||
columns={3}
|
||||
showExcerpts={false}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
@@ -130,6 +200,9 @@ export default function Blog() {
|
||||
<code>postsDisplay.showOnBlogPage</code> in siteConfig to enable.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Footer section */}
|
||||
{showFooter && <Footer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,6 +206,8 @@ export default function Post({
|
||||
// Check if right sidebar is enabled (only when explicitly set in frontmatter)
|
||||
const hasRightSidebar = siteConfig.rightSidebar.enabled && page.rightSidebar === true;
|
||||
const hasAnySidebar = hasLeftSidebar || hasRightSidebar;
|
||||
// Track if only right sidebar is enabled (for centering article)
|
||||
const hasOnlyRightSidebar = hasRightSidebar && !hasLeftSidebar;
|
||||
|
||||
return (
|
||||
<div className={`post-page ${hasAnySidebar ? "post-page-with-sidebar" : ""}`}>
|
||||
@@ -229,7 +231,7 @@ export default function Post({
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className={hasAnySidebar ? "post-content-with-sidebar" : ""}>
|
||||
<div className={`${hasAnySidebar ? "post-content-with-sidebar" : ""} ${hasOnlyRightSidebar ? "post-content-right-sidebar-only" : ""}`}>
|
||||
{/* Left sidebar - TOC */}
|
||||
{hasLeftSidebar && (
|
||||
<aside className="post-sidebar-wrapper post-sidebar-left">
|
||||
@@ -238,7 +240,7 @@ export default function Post({
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<article className={`post-article ${hasAnySidebar ? "post-article-with-sidebar" : ""}`}>
|
||||
<article className={`post-article ${hasAnySidebar ? "post-article-with-sidebar" : ""} ${hasOnlyRightSidebar ? "post-article-centered" : ""}`}>
|
||||
<header className="post-header">
|
||||
<div className="post-title-row">
|
||||
<h1 className="post-title">{page.title}</h1>
|
||||
@@ -334,6 +336,8 @@ export default function Post({
|
||||
// Check if right sidebar is enabled (only when explicitly set in frontmatter)
|
||||
const hasRightSidebar = siteConfig.rightSidebar.enabled && post.rightSidebar === true;
|
||||
const hasAnySidebar = hasLeftSidebar || hasRightSidebar;
|
||||
// Track if only right sidebar is enabled (for centering article)
|
||||
const hasOnlyRightSidebar = hasRightSidebar && !hasLeftSidebar;
|
||||
|
||||
// Render blog post with full metadata
|
||||
return (
|
||||
@@ -361,7 +365,7 @@ export default function Post({
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className={hasAnySidebar ? "post-content-with-sidebar" : ""}>
|
||||
<div className={`${hasAnySidebar ? "post-content-with-sidebar" : ""} ${hasOnlyRightSidebar ? "post-content-right-sidebar-only" : ""}`}>
|
||||
{/* Left sidebar - TOC */}
|
||||
{hasLeftSidebar && (
|
||||
<aside className="post-sidebar-wrapper post-sidebar-left">
|
||||
@@ -369,7 +373,7 @@ export default function Post({
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<article className={`post-article ${hasAnySidebar ? "post-article-with-sidebar" : ""}`}>
|
||||
<article className={`post-article ${hasAnySidebar ? "post-article-with-sidebar" : ""} ${hasOnlyRightSidebar ? "post-article-centered" : ""}`}>
|
||||
<header className="post-header">
|
||||
<div className="post-title-row">
|
||||
<h1 className="post-title">{post.title}</h1>
|
||||
|
||||
@@ -309,6 +309,13 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Mobile and tablet layout padding */
|
||||
@media (max-width: 1024px) {
|
||||
.layout {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 800px;
|
||||
@@ -901,13 +908,51 @@ body {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Adjust main content padding when right sidebar exists */
|
||||
/* Center article in middle column when right sidebar exists */
|
||||
.post-content-with-sidebar:has(.post-sidebar-right)
|
||||
.post-article-with-sidebar {
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 48px;
|
||||
padding-right: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Center article layout when ONLY right sidebar is enabled (no left sidebar) */
|
||||
/* Right sidebar flush right, article centered in remaining space */
|
||||
.post-content-with-sidebar.post-content-right-sidebar-only {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 280px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Centered article styling when only right sidebar exists */
|
||||
.post-article-with-sidebar.post-article-centered {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 48px;
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 1134px) {
|
||||
/* Hide right sidebar on smaller screens, center article fully */
|
||||
.post-content-with-sidebar.post-content-right-sidebar-only {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.post-article-with-sidebar.post-article-centered {
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Left sidebar wrapper - docs-style with alt background and borders */
|
||||
.post-sidebar-wrapper {
|
||||
position: sticky;
|
||||
@@ -992,7 +1037,10 @@ body {
|
||||
min-width: 0; /* Prevent overflow */
|
||||
max-width: 800px;
|
||||
padding-left: 48px;
|
||||
padding-right: 48px;
|
||||
padding-top: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Page sidebar TOC navigation */
|
||||
@@ -1847,6 +1895,20 @@ body {
|
||||
============================================ */
|
||||
.blog-page {
|
||||
padding-top: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
/* List view: constrain to narrower width for readability */
|
||||
.blog-page-list {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* Card view: use full width up to 1200px */
|
||||
.blog-page-cards {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.blog-header {
|
||||
@@ -1878,20 +1940,166 @@ body {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Blog hero section for featured post */
|
||||
.blog-hero-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* Blog featured row section (2-column grid for additional featured posts) */
|
||||
.blog-featured-row {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.blog-featured-row .post-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
/* Blog hero card (large featured card like Giga.ai/news) */
|
||||
.blog-hero-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.blog-hero-card:hover {
|
||||
background-color: var(--bg-hover);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Hero image wrapper */
|
||||
.blog-hero-image-wrapper {
|
||||
aspect-ratio: 16 / 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blog-hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.blog-hero-card:hover .blog-hero-image {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* Hero content section */
|
||||
.blog-hero-content {
|
||||
padding: 32px 32px 32px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Hero tags */
|
||||
.blog-hero-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.blog-hero-tag {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
color: var(--accent-color, var(--text-secondary));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Hero date */
|
||||
.blog-hero-date {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Hero title */
|
||||
.blog-hero-title {
|
||||
font-size: var(--font-size-4xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Hero excerpt */
|
||||
.blog-hero-excerpt {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin: 0 0 20px 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hero footer with author and read more */
|
||||
.blog-hero-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.blog-hero-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.blog-hero-author-image {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.blog-hero-author-name {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.blog-hero-read-time {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.blog-hero-read-more {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Blog post cards grid (thumbnail view) */
|
||||
.post-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
gap: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 2-column layout when there's a hero post */
|
||||
.post-cards-2col {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.post-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
overflow: hidden;
|
||||
@@ -1902,15 +2110,15 @@ body {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Thumbnail image wrapper with square aspect ratio */
|
||||
/* Thumbnail image wrapper with landscape aspect ratio (like Giga.ai) */
|
||||
.post-card-image-wrapper {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
aspect-ratio: 16 / 10;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Image displays as square regardless of original aspect ratio */
|
||||
/* Image displays as landscape regardless of original aspect ratio */
|
||||
.post-card-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -2080,6 +2288,11 @@ body {
|
||||
}
|
||||
|
||||
/* Blog page responsive */
|
||||
.blog-page {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.blog-title {
|
||||
font-size: var(--font-size-blog-page-title);
|
||||
}
|
||||
@@ -3942,12 +4155,39 @@ body {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
/* Blog hero card responsive (tablet) */
|
||||
.blog-hero-card {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.blog-hero-image-wrapper {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.blog-hero-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.blog-hero-title {
|
||||
font-size: var(--font-size-3xl);
|
||||
}
|
||||
|
||||
/* Blog featured row responsive (tablet) */
|
||||
.blog-featured-row .post-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
/* Blog post cards responsive */
|
||||
.post-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.post-cards-2col {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.post-card:not(:has(.post-card-image-wrapper)) {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -4003,6 +4243,40 @@ body {
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Blog page mobile padding */
|
||||
.blog-page {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
/* Blog hero card mobile */
|
||||
.blog-hero-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.blog-hero-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.blog-hero-excerpt {
|
||||
font-size: var(--font-size-sm);
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.blog-hero-footer {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.blog-hero-read-more {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Blog featured row mobile (single column) */
|
||||
.blog-featured-row .post-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.featured-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -4014,6 +4288,11 @@ body {
|
||||
|
||||
.post-cards {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.post-cards-2col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.post-card-image-wrapper {
|
||||
@@ -4228,12 +4507,20 @@ body {
|
||||
text-align: left;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
/* Mobile touch improvements */
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-toc-link:hover {
|
||||
.mobile-menu-toc-link:hover,
|
||||
.mobile-menu-toc-link:active {
|
||||
background-color: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -4243,6 +4530,12 @@ body {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Pressed state for mobile touch feedback */
|
||||
.mobile-menu-toc-link:active {
|
||||
background-color: var(--bg-tertiary, var(--bg-hover));
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.mobile-menu-toc-icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
@@ -4360,7 +4653,7 @@ body {
|
||||
|
||||
.mobile-menu-toc-link {
|
||||
padding: 5px 10px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user