Skip to content

Commit f4fdcf7

Browse files
author
Sadman Hossain
committed
feat: add background, thumbnail colors for posts in light/dark mode
1 parent e2d417f commit f4fdcf7

File tree

5 files changed

+182
-176
lines changed

5 files changed

+182
-176
lines changed

src/components/BlogCard.astro

Lines changed: 72 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
parseAuthors,
1010
} from '@/lib/data-utils'
1111
import { formatDate } from '@/lib/utils'
12+
import { getThumbnailColors } from '@/lib/thumbnail-colors'
1213
import { Icon } from 'astro-icon/components'
1314
import { Image } from 'astro:assets'
1415
import type { CollectionEntry } from 'astro:content'
@@ -24,165 +25,98 @@ const readTime = await getCombinedReadingTime(entry.id)
2425
const authors = await parseAuthors(entry.data.authors ?? [])
2526
const subpostCount = !isSubpost(entry.id) ? await getSubpostCount(entry.id) : 0
2627
27-
// Helper function to get thumbnail colors with automatic pairing
28-
function getThumbnailColors(data: any) {
29-
const theme: 'dark-on-light' | 'light-on-dark' = data.thumbnailTheme === 'light-on-dark' ? 'light-on-dark' : 'dark-on-light' // default theme
30-
31-
// Default color pairs
32-
const colorPairs = {
33-
'dark-on-light': {
34-
iconDefault: '#374151', // gray-700
35-
bgDefault: '#f3f4f6', // gray-100
36-
},
37-
'light-on-dark': {
38-
iconDefault: '#f9fafb', // gray-50
39-
bgDefault: '#374151', // gray-700
40-
}
28+
/* ---------- visual theming ---------- */
29+
const thumb = getThumbnailColors(entry.data) // ← NEW
30+
const card = (() => {
31+
if (!entry.data.cardBgColor) return { hasBg:false }
32+
return {
33+
hasBg:true,
34+
light: entry.data.cardBgColor,
35+
dark: entry.data.cardBgColorDark ?? entry.data.cardBgColor,
4136
}
42-
43-
const defaultColors = colorPairs[theme]
44-
45-
let iconColor = data.thumbnailIconColor || defaultColors.iconDefault
46-
let bgColor = data.thumbnailBgColor || defaultColors.bgDefault
47-
48-
// If only one color is provided, auto-generate the other based on theme
49-
if (data.thumbnailIconColor && !data.thumbnailBgColor) {
50-
// Icon color provided, generate background
51-
if (theme === 'dark-on-light') {
52-
bgColor = lightenColor(data.thumbnailIconColor, 0.85) // Much lighter version
53-
} else {
54-
bgColor = darkenColor(data.thumbnailIconColor, 0.7) // Darker version
55-
}
56-
} else if (data.thumbnailBgColor && !data.thumbnailIconColor) {
57-
// Background color provided, generate icon
58-
if (theme === 'dark-on-light') {
59-
iconColor = darkenColor(data.thumbnailBgColor, 0.7) // Much darker version
60-
} else {
61-
iconColor = lightenColor(data.thumbnailBgColor, 0.85) // Lighter version
62-
}
63-
}
64-
65-
return { iconColor, bgColor }
66-
}
67-
68-
// Helper functions to lighten/darken colors
69-
function lightenColor(color: string, factor: number): string {
70-
// Convert hex to RGB
71-
const hex = color.replace('#', '')
72-
const r = parseInt(hex.substr(0, 2), 16)
73-
const g = parseInt(hex.substr(2, 2), 16)
74-
const b = parseInt(hex.substr(4, 2), 16)
75-
76-
// Lighten by interpolating towards white
77-
const newR = Math.round(r + (255 - r) * factor)
78-
const newG = Math.round(g + (255 - g) * factor)
79-
const newB = Math.round(b + (255 - b) * factor)
80-
81-
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`
82-
}
37+
})()
8338
84-
function darkenColor(color: string, factor: number): string {
85-
// Convert hex to RGB
86-
const hex = color.replace('#', '')
87-
const r = parseInt(hex.substr(0, 2), 16)
88-
const g = parseInt(hex.substr(2, 2), 16)
89-
const b = parseInt(hex.substr(4, 2), 16)
90-
91-
// Darken by multiplying by factor
92-
const newR = Math.round(r * factor)
93-
const newG = Math.round(g * factor)
94-
const newB = Math.round(b * factor)
95-
96-
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`
39+
const cssVars = {
40+
bgLight: card.light,
41+
bgDark: card.dark,
42+
thumbIconLight: thumb.iconLight,
43+
thumbIconDark: thumb.iconDark,
44+
thumbBgLight: thumb.bgLight,
45+
thumbBgDark: thumb.bgDark,
9746
}
47+
---
9848

99-
const thumbnailColors = getThumbnailColors(entry.data)
100-
101-
// Helper function to get card styles
102-
function getCardStyles(data: any) {
103-
let cardStyle = ''
104-
let hoverStyle = ''
105-
106-
// Set background color if provided
107-
if (data.cardBgColor) {
108-
cardStyle += `background-color: ${data.cardBgColor}; `
109-
}
110-
111-
// Set outline color on hover if provided
112-
if (data.cardOutlineColor) {
113-
hoverStyle += `outline: 3px solid ${data.cardOutlineColor}; outline-offset: -3px; `
114-
}
115-
116-
return { cardStyle, hoverStyle }
117-
}
11849

119-
const cardStyles = getCardStyles(entry.data)
120-
---
50+
<style define:vars={cssVars}>
51+
/* card background */
52+
.blog-card-bg { background-color: var(--bgLight); }
53+
:root[data-theme='dark']
54+
.blog-card-bg { background-color: var(--bgDark); }
12155

122-
<style>
123-
/* No image loading styles needed anymore */
56+
/* thumbnail */
57+
.thumbnail { background: var(--thumbBgLight); }
58+
.thumbnail-icon { color: var(--thumbIconLight); }
59+
:root[data-theme='dark']
60+
.thumbnail { background: var(--thumbBgDark); }
61+
:root[data-theme='dark']
62+
.thumbnail-icon { color: var(--thumbIconDark); }
12463
</style>
12564

12665
<div
127-
class="hover:bg-secondary/50 hover-card rounded-xl border p-3 transition-colors duration-300 ease-in-out"
66+
class={`hover:bg-secondary/50 hover-card rounded-xl border p-3 transition-colors duration-300 ease-in-out ${card.hasBg ? 'blog-card-bg' : ''}`}
12867
>
12968
<Link
13069
href={`/${entry.collection}/${entry.id}`}
13170
class="flex flex-col gap-4 sm:flex-row"
13271
>
133-
{
134-
entry.data.image && (
135-
<div class="relative max-w-[200px] sm:flex-shrink-0 relative">
136-
<div
137-
class="h-[55px] w-[80px] rounded-xl flex items-center justify-center"
138-
style={`background-color: ${thumbnailColors.bgColor}`}
139-
>
140-
<Icon
141-
name={entry.data.thumbnailIcon || 'lucide:file-text'}
142-
class={`${entry.data.thumbnailIconSize || 'h-10 w-10'}`}
143-
style={`color: ${thumbnailColors.iconColor}`}
144-
/>
145-
</div>
72+
{(entry.data.thumbnailIcon || entry.data.image) && (
73+
<div class="relative max-w-[200px] sm:flex-shrink-0">
74+
<div class="h-[55px] w-[80px] rounded-xl flex items-center justify-center thumbnail">
75+
<Icon
76+
name={entry.data.thumbnailIcon || 'lucide:file-text'}
77+
class={`thumbnail-icon ${entry.data.thumbnailIconSize || 'h-10 w-10'}`}
78+
style="fill:currentColor;"
79+
/>
14680
</div>
147-
)
148-
}
81+
</div>
82+
)}
14983

15084
<div class="grow">
151-
<h3 class="text-xl font-extrabold leading-none mb-1">{entry.data.title}</h3>
152-
<p class="text-muted-foreground mb-1 text-sm font-medium leading-tight">{entry.data.description}</p>
85+
<h3 class="text-wrap text-xl font-extrabold leading-none mb-1">
86+
{entry.data.title}
87+
</h3>
88+
<p class="text-muted-foreground mb-1 text-sm font-medium leading-tight">
89+
{entry.data.description}
90+
</p>
15391

154-
<div
155-
class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-2 text-xs"
156-
>
92+
<div class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-2 text-xs">
15793
<span>{formattedDate}</span>
15894
<Separator orientation="vertical" className="h-4!" />
15995
<span>{readTime}</span>
160-
{
161-
subpostCount > 0 && (
162-
<>
163-
<Separator orientation="vertical" className="h-4!" />
164-
<span class="flex items-center gap-1">
165-
<Icon name="lucide:file-text" class="size-3" />
166-
{subpostCount} subpost{subpostCount === 1 ? '' : 's'}
167-
</span>
168-
</>
169-
)
170-
}
171-
{
172-
entry.data.tags && entry.data.tags.length > 0 && (
173-
<>
174-
<Separator orientation="vertical" className="h-4!" />
175-
<div class="flex flex-wrap gap-1">
176-
{entry.data.tags.map((tag) => (
177-
<Badge variant="muted" className="flex items-center gap-x-1 text-xs">
178-
{tag}
179-
</Badge>
180-
))}
181-
</div>
182-
</>
183-
)
184-
}
96+
97+
{subpostCount > 0 && (
98+
<>
99+
<Separator orientation="vertical" className="h-4!" />
100+
<span class="flex items-center gap-1">
101+
<Icon name="lucide:file-text" class="size-3" />
102+
{subpostCount} subpost{subpostCount === 1 ? '' : 's'}
103+
</span>
104+
</>
105+
)}
106+
107+
{entry.data.tags?.length && (
108+
<>
109+
<Separator orientation="vertical" className="h-4!" />
110+
<div class="flex flex-wrap gap-1">
111+
{entry.data.tags.map((tag) => (
112+
<Badge variant="muted" className="flex items-center gap-x-1 text-xs">
113+
{tag}
114+
</Badge>
115+
))}
116+
</div>
117+
</>
118+
)}
185119
</div>
186120
</div>
187121
</Link>
188-
</div>
122+
</div>

src/content.config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ const blog = defineCollection({
188188
title: z.string(),
189189
description: z.string(),
190190
date: z.coerce.date(),
191+
slug: z.string().optional(),
191192
order: z.number().optional(),
192193
image: image().optional(),
193194
tags: z.array(z.string()).optional(),
@@ -197,11 +198,16 @@ const blog = defineCollection({
197198
thumbnailIcon: z.string().optional(),
198199
thumbnailIconSize: z.string().optional(),
199200
thumbnailIconColor: z.string().optional(),
201+
thumbnailIconColorDark: z.string().optional(),
200202
thumbnailBgColor: z.string().optional(),
203+
thumbnailBgColorDark: z.string().optional(),
201204
thumbnailTheme: z.enum(['dark-on-light', 'light-on-dark']).optional(),
202205
// Blog card styling fields
206+
// cardBgColor accepts OKLCH colors for light mode
207+
// cardBgColorDark accepts OKLCH colors for dark mode (optional, falls back to cardBgColor)
208+
// Examples: 'oklch(0.95 0.02 240)', 'oklch(0.85 0.1 120)'
203209
cardBgColor: z.string().optional(),
204-
cardOutlineColor: z.string().optional(),
210+
cardBgColorDark: z.string().optional(),
205211
}),
206212
})
207213

src/lib/thumbnail-colors.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export type ThumbnailColors = {
2+
iconLight: string;
3+
iconDark: string;
4+
bgLight: string;
5+
bgDark: string;
6+
};
7+
8+
export function getThumbnailColors(data: Record<string, any>): ThumbnailColors {
9+
const theme: 'dark-on-light' | 'light-on-dark' =
10+
data.thumbnailTheme === 'light-on-dark' ? 'light-on-dark' : 'dark-on-light';
11+
12+
const defaults = {
13+
'dark-on-light': { icon: '#374151', bg: '#f3f4f6' }, // gray-700 / gray-100
14+
'light-on-dark': { icon: '#f9fafb', bg: '#374151' }, // gray-50 / gray-700
15+
}[theme];
16+
17+
const iconLight = data.thumbnailIconColor ?? defaults.icon;
18+
const iconDark = data.thumbnailIconColorDark ?? iconLight;
19+
const bgLight = data.thumbnailBgColor ?? defaults.bg;
20+
const bgDark = data.thumbnailBgColorDark ?? bgLight;
21+
22+
return { iconLight, iconDark, bgLight, bgDark };
23+
}

src/pages/blog/[...id].astro

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
parseAuthors,
2525
} from '@/lib/data-utils'
2626
import { formatDate } from '@/lib/utils'
27+
import { getThumbnailColors } from '@/lib/thumbnail-colors'
2728
import { Icon } from 'astro-icon/components'
2829
import { Image } from 'astro:assets'
2930
import { render } from 'astro:content'
@@ -56,10 +57,24 @@ const combinedReadingTime =
5657
: null
5758
5859
const tocSections = await getTOCSections(currentPostId)
60+
61+
const thumb = getThumbnailColors(post.data);
62+
const cssVars = {
63+
thumbIconLight: thumb.iconLight,
64+
thumbIconDark: thumb.iconDark,
65+
thumbBgLight: thumb.bgLight,
66+
thumbBgDark: thumb.bgDark,
67+
};
5968
---
6069

61-
<style>
62-
/* No image loading styles needed anymore */
70+
<style define:vars={cssVars}>
71+
.post-thumbnail { background-color: var(--thumbBgLight); }
72+
.post-thumbnail-icon { color: var(--thumbIconLight); }
73+
74+
:root[data-theme='dark']
75+
.post-thumbnail { background-color: var(--thumbBgDark); }
76+
:root[data-theme='dark']
77+
.post-thumbnail-icon { color: var(--thumbIconDark); }
6378
</style>
6479

6580
<Layout>
@@ -122,21 +137,19 @@ const tocSections = await getTOCSections(currentPostId)
122137
</div>
123138

124139
{
125-
post.data.image && (
126-
<div class="relative col-span-full mx-auto w-full max-w-[200px]" style="padding-left: 0 !important; padding-right: 0!important;">
127-
<div
128-
class="aspect-[300/157.5] rounded-xl flex items-center justify-center"
129-
style={`background-color: ${post.data.thumbnailBgColor || '#f3f4f6'}`}
130-
>
131-
<Icon
132-
name={post.data.thumbnailIcon || 'lucide:file-text'}
133-
class={`${post.data.thumbnailIconSize || 'h-16 w-16'}`}
134-
style={`color: ${post.data.thumbnailIconColor || '#6b7280'}`}
135-
/>
140+
post.data.thumbnailIcon &&
141+
(
142+
<div class="relative col-span-full mx-auto w-full max-w-[150px] px-0">
143+
<div class="aspect-[300/157.5] rounded-xl flex items-center justify-center post-thumbnail">
144+
<Icon
145+
name={post.data.thumbnailIcon}
146+
class={`post-thumbnail-icon ${post.data.thumbnailIconSize || 'h-16 w-16'}`}
147+
style="fill:currentColor;"
148+
/>
149+
</div>
136150
</div>
137-
</div>
138151
)
139-
}
152+
}
140153

141154
<section class="col-start-2 flex flex-col gap-y-6 text-center">
142155
<div class="flex flex-col">

0 commit comments

Comments
 (0)