Skip to content

Commit e2bc148

Browse files
committed
chore(blog): updated snippets
1 parent 4180c75 commit e2bc148

13 files changed

Lines changed: 210 additions & 128 deletions

apps/blog/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"class-variance-authority": "^0.7.1",
1212
"codice": "^1.3.2",
1313
"date-fns": "^3.6.0",
14-
"effect": "^3.14.14",
14+
"effect": "^3.15.2",
1515
"esbuild": "^0.25.4",
1616
"framer-motion": "^12.7.4",
1717
"jose": "^6.0.11",
@@ -28,12 +28,12 @@
2828
},
2929
"devDependencies": {
3030
"@blog/utils": "workspace:*",
31-
"@shadcn/ui": "workspace:*",
3231
"@effect/cli": "^0.59.18",
3332
"@effect/platform-node": "^0.77.6",
3433
"@mdx-js/loader": "^3.1.0",
3534
"@mdx-js/react": "^3.1.0",
3635
"@next/mdx": "^15.3.1",
36+
"@shadcn/ui": "workspace:*",
3737
"@tailwindcss/postcss": "^4.1.4",
3838
"@tailwindcss/typography": "^0.5.16",
3939
"@types/mdx": "^2.0.13",

apps/blog/src/app/(public)/blog/page.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,6 @@ export default function BlogPage() {
3939
<Page>
4040
<PageSection>
4141
<PageHeading>Blog</PageHeading>
42-
<div className="max-w-2xl">
43-
<p className="hidden leading-relaxed text-muted-foreground">
44-
No noise, just ideas—sorted to make browsing easier. Whether
45-
you&apos;re curious about tech, leadership, or critical thinking,
46-
everything here is organized by category.
47-
</p>
48-
</div>
4942

5043
{/* Categories */}
5144
<div className="mb-8">

apps/blog/src/app/(public)/snippets/snippet-list.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { slugify } from '@blog/utils'
3+
import { slugify, stripAnsiCodes } from '@blog/utils'
44
import {
55
Badge,
66
Button,
@@ -35,7 +35,7 @@ import { ButtonGroup } from '@/components/button-group'
3535
import { useNonce } from '@/components/context/nonce-context'
3636
import { Flex } from '@/components/flex'
3737
import { InlineLink } from '@/components/inline-link'
38-
import { getSnippetBySlug, getSnippets, Snippet } from '@/lib/snippets'
38+
import { getLatestSnippets, getSnippetBySlug, Snippet } from '@/lib/snippets'
3939

4040
const NONCE_HEADER = String('nonce')
4141

@@ -55,7 +55,7 @@ export default function SnippetList() {
5555

5656
React.useEffect(() => {
5757
const loadSnippets = async () => {
58-
const snippets = getSnippets()
58+
const snippets = getLatestSnippets()
5959
setSnippets(snippets)
6060
setLoading(false)
6161
}
@@ -149,19 +149,23 @@ export default function SnippetList() {
149149
onClick={() => handleOpenSnippet(snippet.slug)}
150150
className="text-left p-0"
151151
>
152-
<div className="flex flex-row items-center gap-4 justify-between w-full">
152+
<div className="flex flex-row items-center gap-4 justify-between w-full hover:text-muted-foreground">
153153
<Flex>
154-
<p className="text-1xl">{snippet.title}</p>
155-
<p className="text-muted-foreground line-clamp-2 mb-4">
154+
<h2 className="text-xl mb-1 transition-colors">
155+
{snippet.title}
156+
</h2>
157+
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
156158
{snippet.description}
157159
</p>
158-
<p className="text-sm text-muted-foreground">
160+
<p className="text-xs text-muted-foreground">
159161
{formatDistanceToNow(new Date(snippet.createdAt), {
160162
addSuffix: true,
161163
})}
162164
</p>
163165
</Flex>
164-
<Badge className="capitalize">{snippet.language}</Badge>
166+
<Badge variant="outline" className="capitalize">
167+
{snippet.language}
168+
</Badge>
165169
</div>
166170
</InlineLink>
167171
))}
@@ -188,7 +192,9 @@ export default function SnippetList() {
188192
</p>
189193
)}
190194
<div className="flex items-center gap-2">
191-
<Badge>{selectedSnippet.language}</Badge>
195+
<Badge className="capitalize">
196+
{selectedSnippet.language}
197+
</Badge>
192198
<span className="text-sm text-muted-foreground">
193199
Created{' '}
194200
{formatDistanceToNow(new Date(selectedSnippet.createdAt), {
@@ -237,6 +243,7 @@ export default function SnippetList() {
237243
>
238244
{selectedSnippet.code}
239245
</Code>
246+
240247
<ScrollBar orientation="horizontal" />
241248
</ScrollArea>
242249
</TabsContent>
@@ -260,7 +267,9 @@ export default function SnippetList() {
260267
</Button>
261268

262269
<ScrollArea className="h-[64vh] rounded-md bg-muted p-4 text-sm font-mono whitespace-pre-wrap">
263-
<Code>{output.map((line) => line).join('\n')}</Code>
270+
<Code>
271+
{output.map((line) => stripAnsiCodes(line)).join('\n')}
272+
</Code>
264273
<ScrollBar orientation="vertical" />
265274
</ScrollArea>
266275
</div>
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2+
"date": "2025-05-19",
23
"title": "Effect CLI with Options",
3-
"description": "A practical example using `@effect/cli` to build a typed, composable CLI with default values and optional parameters.",
4-
"language": "TypeScript"
4+
"description": "Build a typed CLI with aliases, defaults, and flags using `@effect/cli`.",
5+
"language": "typescript"
56
}
Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,47 @@
1-
import { join } from 'path'
2-
31
import { Command, Options } from '@effect/cli'
42
import { NodeContext, NodeRuntime } from '@effect/platform-node'
5-
import { Console, Effect, Option, pipe } from 'effect'
3+
import { Console, Effect } from 'effect'
64

7-
const file = Options.text('file').pipe(
8-
Options.withAlias('f'),
9-
Options.withDefault('./config.json'),
10-
Options.withDescription('Path to the configuration file')
5+
const skip = Options.boolean('skip').pipe(
6+
Options.withAlias('s'),
7+
Options.withDescription('Skip prompts and use defaults')
118
)
129

13-
const content = Options.text('content').pipe(
14-
Options.optional, // this makes it an Option
10+
const config = Options.text('config').pipe(
1511
Options.withAlias('c'),
16-
Options.withDefault(Option.some('src/content')),
17-
Options.withDescription('Path to the content directory')
12+
Options.withDefault('default.json'),
13+
Options.withDescription('Path to the JSON configuration file')
1814
)
1915

20-
const blogx = Command.make('blogx', { file, content }, (o) => {
21-
// these will be printed
22-
console.log('Config', `"${o.file}"`)
23-
24-
// this will not be printed
25-
Console.log('Add .pipe(NodeRuntime.runMain) to print this.')
26-
27-
// example using pipe
28-
pipe(
29-
Effect.succeed(o.content),
30-
Effect.map((content) => {
31-
if (Option.isNone(content)) {
32-
return console.warn('No content directory, skipping.')
33-
}
34-
return console.log(`${join(process.cwd(), content.toString())}`)
35-
}),
36-
Effect.catchAll((e) => {
37-
console.error('Error:', e)
38-
return Effect.succeed(0)
39-
})
40-
)
41-
42-
// example using option match
43-
Option.match(o.content, {
44-
onNone: () =>
45-
Effect.succeed(console.warn('No content directory, skipping.')),
46-
onSome: (dir) => Effect.succeed(console.log(`${join(process.cwd(), dir)}`)),
47-
}).pipe(NodeRuntime.runMain)
16+
let count = 1
4817

49-
// example using generator
18+
const init = Command.make('init', { skip, config }, ({ skip, config }) =>
5019
Effect.gen(function* () {
51-
yield* Effect.succeed(
52-
Option.isNone(o.content)
53-
? console.log('Content OK ✅')
54-
: console.warn('Content FAIL ❌')
55-
)
56-
57-
// throws 💥
58-
// yield* Effect.fail(new Error('Effect.fail works like a throw here'))
59-
60-
// this would not be executed
61-
yield* Console.log('I would never run if the previous effect had failed')
62-
}).pipe(NodeRuntime.runMain)
63-
64-
// this would never execute
65-
console.error("The previous failure would bubble up, I'd never be printed")
20+
yield* Console.log('run:', count++)
21+
yield* Console.log('-c', config)
22+
yield* Console.log('--skip', skip, '\n')
23+
})
24+
)
6625

67-
// we could exit with a failure
68-
return Effect.exit(Effect.succeed(0))
26+
const cli = Command.run(init, {
27+
name: 'my-cli',
28+
version: '1.0.0',
6929
})
7030

71-
// cli program
72-
const cli = Command.run(blogx, {
73-
name: 'blogx cli',
74-
version: 'v0.0.1',
75-
})
31+
// tsx cli.ts --skip
32+
cli(['', '', '--skip']).pipe(
33+
Effect.provide(NodeContext.layer),
34+
NodeRuntime.runMain
35+
)
36+
37+
// tsx cli.ts -c my-config.json
38+
cli(['', '', '-c', 'my-config.json']).pipe(
39+
Effect.provide(NodeContext.layer),
40+
NodeRuntime.runMain
41+
)
7642

77-
// tsx effect-cli.ts -f src/info.json -c src/content
78-
export default cli(process.argv).pipe(
43+
// tsx cli.ts --help
44+
cli(['', '', '--help']).pipe(
7945
Effect.provide(NodeContext.layer),
8046
NodeRuntime.runMain
8147
)
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2+
"date": "2025-05-18",
23
"title": "JWT for Beginners",
34
"description": "A simple TypeScript example demonstrating password hashing with `bcryptjs` and JWT token signing and verification using `jose`.",
4-
"language": "TypeScript"
5+
"language": "typescript"
56
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"date": "2025-05-20",
3+
"title": "Shopping Cart Pricing with `pipe` in TypeScript",
4+
"description": "Calculate taxes, discounts, and totals in a cart using Effect's `pipe` for clean, composable logic.",
5+
"language": "typescript"
6+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { pipe } from 'effect'
2+
3+
type Product = {
4+
id: string
5+
sku: string
6+
name: string
7+
price: number
8+
category: 'food' | 'electronics' | 'clothing'
9+
quantity: number
10+
}
11+
12+
const cart: Product[] = [
13+
{
14+
id: '1',
15+
sku: crypto.randomUUID(),
16+
name: 'Bread',
17+
price: 3.5,
18+
category: 'food',
19+
quantity: 2,
20+
},
21+
{
22+
id: '2',
23+
sku: crypto.randomUUID(),
24+
name: 'T-Shirt',
25+
price: 20,
26+
category: 'clothing',
27+
quantity: 1,
28+
},
29+
{
30+
id: '3',
31+
sku: crypto.randomUUID(),
32+
name: 'Headphones',
33+
price: 99.99,
34+
category: 'electronics',
35+
quantity: 1,
36+
},
37+
]
38+
39+
const applyTax = (product: Product): Product & { taxedPrice: number } => {
40+
const taxRates = { food: 0.05, clothing: 0.1, electronics: 0.2 }
41+
const taxRate = taxRates[product.category] ?? 0
42+
const taxedPrice = product.price * (1 + taxRate)
43+
return { ...product, taxedPrice }
44+
}
45+
46+
// Discount Logic
47+
const applyDiscount = (
48+
product: Product & { taxedPrice: number }
49+
): Product & { finalPrice: number } => {
50+
// $10 off electronics over $100 (pre-tax)
51+
if (product.category === 'electronics' && product.price >= 100) {
52+
return { ...product, finalPrice: product.taxedPrice - 10 }
53+
}
54+
55+
// 10% off clothing
56+
if (product.category === 'clothing') {
57+
return { ...product, finalPrice: product.taxedPrice * 0.9 }
58+
}
59+
60+
// No discount
61+
return { ...product, finalPrice: product.taxedPrice }
62+
}
63+
64+
// Calculate Total Per Item
65+
const computeTotalPerItem = (product: Product & { finalPrice: number }) => ({
66+
...product,
67+
total: product.finalPrice * product.quantity,
68+
})
69+
70+
// Pipeline to Process One Product 🏋🏻‍♀️
71+
const processProduct = (product: Product) =>
72+
pipe(product, applyTax, applyDiscount, computeTotalPerItem)
73+
74+
// Transform the Entire Cart 🛒
75+
const processedCart = cart.map(processProduct)
76+
console.log('Processed Cart:', processedCart)
77+
78+
// Summarize Cart Totals ➕
79+
const sum = (a: number, b: number) => a + b
80+
81+
const cartTotal = pipe(
82+
processedCart.map((p) => p.total),
83+
(totals) => totals.reduce(sum, 0)
84+
)
85+
86+
console.log('Cart Total:', cartTotal.toFixed(2)) // Cart Total: 147.14 💵

apps/blog/src/components/snippet-view.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { slugify } from '@blog/utils'
3+
import { slugify, stripAnsiCodes } from '@blog/utils'
44
import {
55
Badge,
66
Button,
@@ -127,7 +127,7 @@ export function SnippetView({ data }: { data: Snippet }) {
127127
<p className="text-muted-foreground">{snippet.description}</p>
128128
)}
129129
<div className="flex items-center gap-2">
130-
<Badge>{snippet.language}</Badge>
130+
<Badge className="capitalize">{snippet.language}</Badge>
131131
<span className="text-sm text-muted-foreground">
132132
Created{' '}
133133
{formatDistanceToNow(new Date(snippet.createdAt), {
@@ -177,7 +177,9 @@ export function SnippetView({ data }: { data: Snippet }) {
177177
</Button>
178178

179179
<ScrollArea className="h-[64vh] rounded-md bg-muted p-4 text-sm font-mono whitespace-pre-wrap">
180-
<Code>{output.map((line) => line).join('\n')}</Code>
180+
<Code>
181+
{output.map((line) => stripAnsiCodes(line)).join('\n')}
182+
</Code>
181183
<ScrollBar orientation="vertical" />
182184
</ScrollArea>
183185
</div>

apps/blog/src/lib/snippets.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ import { memoize } from '@blog/utils'
22

33
import snippets from '@/data/snippets.json'
44

5+
export type Snippet = (typeof snippets)[number]
6+
57
export const getSnippets = memoize(() => snippets)
68

7-
export type Snippet = (typeof snippets)[number]
9+
export const getLatestSnippets = memoize(() => {
10+
return getSnippets().sort(
11+
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
12+
)
13+
})
814

915
export function getSnippetById(id: string) {
1016
const snippet = getSnippets().find((snippet) => snippet.id === id)

0 commit comments

Comments
 (0)