Skip to content

Commit 37bc21d

Browse files
committed
Add example app with actions, components, and routes
Introduces a full-featured example application under the 'example' directory, including Beam action handlers, authentication, session management, reusable React components, modal and drawer UIs, and product/cart logic. Updates .gitignore to exclude example build artifacts. This provides a reference implementation for Beam-based apps with product management, cart functionality, authentication, and UI patterns.
1 parent 8f9fde6 commit 37bc21d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3988
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
/node_modules
2+
/example/node_modules
3+
/example/dist
4+
/example/.wrangler

example/app/actions/addToCart.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { HtmlEscapedString } from 'hono/utils/html'
2+
import type { BeamContext } from '@benqoder/beam'
3+
import type { Env } from '../types'
4+
import { CartBadge } from '../components/CartBadge'
5+
6+
function render(node: HtmlEscapedString | Promise<HtmlEscapedString>): Promise<string> {
7+
return Promise.resolve(node).then((n) => n.toString())
8+
}
9+
10+
type CartItem = { productId: string; qty: number }
11+
12+
export async function addToCart(
13+
ctx: BeamContext<Env>,
14+
{ productId, qty = 1 }: Record<string, unknown>
15+
): Promise<string> {
16+
// Use automatic session from ctx.session
17+
const cart = (await ctx.session.get<CartItem[]>('cart')) || []
18+
19+
const existing = cart.find((item) => item.productId === productId)
20+
if (existing) {
21+
existing.qty += Number(qty)
22+
} else {
23+
cart.push({ productId: productId as string, qty: Number(qty) })
24+
}
25+
26+
await ctx.session.set('cart', cart)
27+
28+
const count = cart.reduce((sum, item) => sum + item.qty, 0)
29+
return render(<CartBadge count={count} />)
30+
}

example/app/actions/auth.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { HtmlEscapedString } from 'hono/utils/html'
2+
import type { BeamContext } from '@benqoder/beam'
3+
import type { Env } from '../types'
4+
5+
function render(node: HtmlEscapedString | Promise<HtmlEscapedString>): Promise<string> {
6+
return Promise.resolve(node).then((n) => n.toString())
7+
}
8+
9+
/**
10+
* Get current user info - demonstrates auth context in Beam actions
11+
*/
12+
export async function getCurrentUser(
13+
ctx: BeamContext<Env>,
14+
_data: Record<string, unknown>
15+
): Promise<string> {
16+
if (ctx.user) {
17+
return render(
18+
<div class="user-info authenticated">
19+
<div class="user-avatar">{(ctx.user.name as string)?.[0] || '?'}</div>
20+
<div class="user-details">
21+
<div class="user-name">{ctx.user.name as string}</div>
22+
<div class="user-email">{ctx.user.email as string}</div>
23+
<span class={`badge badge-${ctx.user.role}`}>{ctx.user.role as string}</span>
24+
</div>
25+
<style>{`
26+
.user-info.authenticated {
27+
display: flex;
28+
align-items: center;
29+
gap: 1rem;
30+
}
31+
.user-avatar {
32+
width: 48px;
33+
height: 48px;
34+
background: #3b82f6;
35+
color: white;
36+
border-radius: 50%;
37+
display: flex;
38+
align-items: center;
39+
justify-content: center;
40+
font-size: 1.2rem;
41+
font-weight: bold;
42+
}
43+
.user-details .user-name {
44+
font-weight: 600;
45+
}
46+
.user-details .user-email {
47+
color: #666;
48+
font-size: 0.9rem;
49+
}
50+
.badge {
51+
display: inline-block;
52+
padding: 0.2rem 0.5rem;
53+
border-radius: 4px;
54+
font-size: 0.7rem;
55+
font-weight: 500;
56+
text-transform: uppercase;
57+
margin-top: 0.25rem;
58+
}
59+
.badge-admin { background: #fee2e2; color: #dc2626; }
60+
.badge-user { background: #dbeafe; color: #2563eb; }
61+
.badge-guest { background: #f3f4f6; color: #6b7280; }
62+
`}</style>
63+
</div>
64+
)
65+
}
66+
return render(
67+
<div class="user-info guest">
68+
<span>Not signed in - </span>
69+
<a href="/login">Sign In</a>
70+
<style>{`
71+
.user-info.guest {
72+
color: #666;
73+
}
74+
.user-info.guest a {
75+
color: #3b82f6;
76+
text-decoration: none;
77+
}
78+
.user-info.guest a:hover {
79+
text-decoration: underline;
80+
}
81+
`}</style>
82+
</div>
83+
)
84+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { HtmlEscapedString } from 'hono/utils/html'
2+
import type { BeamContext } from '@benqoder/beam'
3+
import type { Env } from '../types'
4+
import { ProductList } from '../components/ProductList'
5+
6+
function render(node: HtmlEscapedString | Promise<HtmlEscapedString>): Promise<string> {
7+
return Promise.resolve(node).then(n => n.toString())
8+
}
9+
10+
export async function createProduct(ctx: BeamContext<Env>, { name, price }: Record<string, unknown>): Promise<string> {
11+
await ctx.env.DB.prepare('INSERT INTO products (name, price) VALUES (?, ?)')
12+
.bind(name, price)
13+
.run()
14+
15+
const products = await ctx.env.DB.prepare('SELECT * FROM products ORDER BY created_at DESC').all()
16+
return render(<ProductList products={products.results as Array<{ id: string; name: string; price: number }>} />)
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { HtmlEscapedString } from 'hono/utils/html'
2+
import type { BeamContext } from '@benqoder/beam'
3+
import type { Env } from '../types'
4+
import { ProductList } from '../components/ProductList'
5+
6+
function render(node: HtmlEscapedString | Promise<HtmlEscapedString>): Promise<string> {
7+
return Promise.resolve(node).then(n => n.toString())
8+
}
9+
10+
export async function deleteProduct(ctx: BeamContext<Env>, { id }: Record<string, unknown>): Promise<string> {
11+
await ctx.env.DB.prepare('DELETE FROM products WHERE id = ?').bind(id).run()
12+
13+
const products = await ctx.env.DB.prepare('SELECT * FROM products ORDER BY created_at DESC').all()
14+
return render(<ProductList products={products.results as Array<{ id: string; name: string; price: number }>} />)
15+
}

0 commit comments

Comments
 (0)