@basicbenframework/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +35 -0
- package/README.md +588 -0
- package/bin/cli.js +8 -0
- package/create-basicben-app/index.js +205 -0
- package/create-basicben-app/package.json +30 -0
- package/create-basicben-app/template/.env.example +24 -0
- package/create-basicben-app/template/README.md +59 -0
- package/create-basicben-app/template/basicben.config.js +33 -0
- package/create-basicben-app/template/index.html +54 -0
- package/create-basicben-app/template/migrations/001_create_users.js +15 -0
- package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
- package/create-basicben-app/template/public/.gitkeep +0 -0
- package/create-basicben-app/template/seeds/01_users.js +29 -0
- package/create-basicben-app/template/seeds/02_posts.js +43 -0
- package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
- package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
- package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
- package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
- package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
- package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
- package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
- package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
- package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
- package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
- package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
- package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
- package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
- package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
- package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
- package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
- package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
- package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
- package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
- package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
- package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
- package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
- package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
- package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
- package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
- package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
- package/create-basicben-app/template/src/controllers/PostController.js +86 -0
- package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
- package/create-basicben-app/template/src/helpers/api.js +24 -0
- package/create-basicben-app/template/src/main.jsx +9 -0
- package/create-basicben-app/template/src/middleware/auth.js +16 -0
- package/create-basicben-app/template/src/models/Post.js +63 -0
- package/create-basicben-app/template/src/models/User.js +42 -0
- package/create-basicben-app/template/src/routes/App.jsx +38 -0
- package/create-basicben-app/template/src/routes/api/auth.js +7 -0
- package/create-basicben-app/template/src/routes/api/posts.js +15 -0
- package/create-basicben-app/template/src/routes/api/profile.js +8 -0
- package/create-basicben-app/template/src/server/index.js +16 -0
- package/create-basicben-app/template/vite.config.js +18 -0
- package/database.sqlite +0 -0
- package/my-test-app/.env.example +24 -0
- package/my-test-app/README.md +59 -0
- package/my-test-app/basicben.config.js +33 -0
- package/my-test-app/database.sqlite-shm +0 -0
- package/my-test-app/database.sqlite-wal +0 -0
- package/my-test-app/index.html +54 -0
- package/my-test-app/migrations/001_create_users.js +15 -0
- package/my-test-app/migrations/002_create_posts.js +18 -0
- package/my-test-app/package-lock.json +2160 -0
- package/my-test-app/package.json +29 -0
- package/my-test-app/public/.gitkeep +0 -0
- package/my-test-app/seeds/01_users.js +29 -0
- package/my-test-app/seeds/02_posts.js +43 -0
- package/my-test-app/src/client/components/Alert.jsx +11 -0
- package/my-test-app/src/client/components/Avatar.jsx +11 -0
- package/my-test-app/src/client/components/BackLink.jsx +10 -0
- package/my-test-app/src/client/components/Button.jsx +19 -0
- package/my-test-app/src/client/components/Card.jsx +10 -0
- package/my-test-app/src/client/components/Empty.jsx +6 -0
- package/my-test-app/src/client/components/Input.jsx +12 -0
- package/my-test-app/src/client/components/Loading.jsx +6 -0
- package/my-test-app/src/client/components/Logo.jsx +40 -0
- package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
- package/my-test-app/src/client/components/NavLink.jsx +10 -0
- package/my-test-app/src/client/components/PageHeader.jsx +8 -0
- package/my-test-app/src/client/components/PostCard.jsx +19 -0
- package/my-test-app/src/client/components/Textarea.jsx +12 -0
- package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
- package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
- package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
- package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
- package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
- package/my-test-app/src/client/pages/Auth.jsx +55 -0
- package/my-test-app/src/client/pages/Authentication.jsx +236 -0
- package/my-test-app/src/client/pages/Database.jsx +426 -0
- package/my-test-app/src/client/pages/Feed.jsx +34 -0
- package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
- package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
- package/my-test-app/src/client/pages/Home.jsx +206 -0
- package/my-test-app/src/client/pages/PostForm.jsx +69 -0
- package/my-test-app/src/client/pages/Posts.jsx +59 -0
- package/my-test-app/src/client/pages/Profile.jsx +68 -0
- package/my-test-app/src/client/pages/Routing.jsx +207 -0
- package/my-test-app/src/client/pages/Testing.jsx +251 -0
- package/my-test-app/src/client/pages/Validation.jsx +210 -0
- package/my-test-app/src/controllers/AuthController.js +81 -0
- package/my-test-app/src/controllers/HomeController.js +17 -0
- package/my-test-app/src/controllers/PostController.js +86 -0
- package/my-test-app/src/controllers/ProfileController.js +66 -0
- package/my-test-app/src/helpers/api.js +24 -0
- package/my-test-app/src/main.jsx +9 -0
- package/my-test-app/src/middleware/auth.js +16 -0
- package/my-test-app/src/models/Post.js +63 -0
- package/my-test-app/src/models/User.js +42 -0
- package/my-test-app/src/routes/App.jsx +38 -0
- package/my-test-app/src/routes/api/auth.js +7 -0
- package/my-test-app/src/routes/api/posts.js +15 -0
- package/my-test-app/src/routes/api/profile.js +8 -0
- package/my-test-app/src/server/index.js +16 -0
- package/my-test-app/vite.config.js +18 -0
- package/package.json +61 -0
- package/scripts/test-app.sh +59 -0
- package/src/auth/jwt.js +195 -0
- package/src/auth/password.js +132 -0
- package/src/cli/colors.js +31 -0
- package/src/cli/dispatcher.js +168 -0
- package/src/cli/parser.js +91 -0
- package/src/client/context.js +4 -0
- package/src/client/hooks.js +50 -0
- package/src/client/index.js +3 -0
- package/src/client/router.js +184 -0
- package/src/commands/build.js +155 -0
- package/src/commands/dev.js +206 -0
- package/src/commands/help.js +84 -0
- package/src/commands/make-controller.js +36 -0
- package/src/commands/make-middleware.js +44 -0
- package/src/commands/make-migration.js +51 -0
- package/src/commands/make-model.js +38 -0
- package/src/commands/make-route.js +36 -0
- package/src/commands/make-seed.js +38 -0
- package/src/commands/migrate-fresh.js +32 -0
- package/src/commands/migrate-rollback.js +30 -0
- package/src/commands/migrate-status.js +41 -0
- package/src/commands/migrate.js +30 -0
- package/src/commands/seed.js +47 -0
- package/src/commands/start.js +69 -0
- package/src/commands/test.js +46 -0
- package/src/db/Grammar.js +125 -0
- package/src/db/QueryBuilder.js +476 -0
- package/src/db/adapters/neon.js +170 -0
- package/src/db/adapters/planetscale.js +146 -0
- package/src/db/adapters/postgres.js +166 -0
- package/src/db/adapters/sqlite.js +125 -0
- package/src/db/adapters/turso.js +165 -0
- package/src/db/index.js +156 -0
- package/src/db/migrator.js +250 -0
- package/src/db/seeder.js +124 -0
- package/src/index.js +12 -0
- package/src/scaffolding/index.js +152 -0
- package/src/server/body-parser.js +159 -0
- package/src/server/cors.js +63 -0
- package/src/server/default-entry.js +13 -0
- package/src/server/http.js +221 -0
- package/src/server/index.js +168 -0
- package/src/server/loader.js +128 -0
- package/src/server/router.js +281 -0
- package/src/server/static.js +139 -0
- package/src/validation/index.js +436 -0
- package/src/vite/config.js +49 -0
- package/stubs/controller.stub +48 -0
- package/stubs/middleware-auth.stub +29 -0
- package/stubs/middleware.stub +9 -0
- package/stubs/migration.stub +17 -0
- package/stubs/model.stub +77 -0
- package/stubs/route.stub +13 -0
- package/stubs/seed.stub +16 -0
- package/stubs/vite.config.stub +18 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useAuth, useNavigate } from '@basicbenframework/core/client'
|
|
2
|
+
import { useTheme } from '../components/ThemeContext'
|
|
3
|
+
import { Card } from '../components/Card'
|
|
4
|
+
import { Button } from '../components/Button'
|
|
5
|
+
import { Avatar } from '../components/Avatar'
|
|
6
|
+
import { Logo } from '../components/Logo'
|
|
7
|
+
|
|
8
|
+
export function Home() {
|
|
9
|
+
const { user } = useAuth()
|
|
10
|
+
const navigate = useNavigate()
|
|
11
|
+
const { t, dark } = useTheme()
|
|
12
|
+
|
|
13
|
+
const features = [
|
|
14
|
+
{
|
|
15
|
+
icon: (
|
|
16
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
17
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
18
|
+
</svg>
|
|
19
|
+
),
|
|
20
|
+
title: 'Zero Dependencies',
|
|
21
|
+
desc: 'No runtime deps. HTTP server, router, JWT, validation — all written from scratch using Node.js built-ins.'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
icon: (
|
|
25
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
26
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
27
|
+
</svg>
|
|
28
|
+
),
|
|
29
|
+
title: 'Laravel-Inspired DX',
|
|
30
|
+
desc: 'Migrations, controllers, models, and scaffolding commands. Familiar conventions without the magic.'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
icon: (
|
|
34
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
35
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" />
|
|
36
|
+
</svg>
|
|
37
|
+
),
|
|
38
|
+
title: 'No Lock-in',
|
|
39
|
+
desc: 'Just React, Node.js, and Vite. You own your stack. Eject anytime — your code is still your code.'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
icon: (
|
|
43
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
44
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
45
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
46
|
+
</svg>
|
|
47
|
+
),
|
|
48
|
+
title: 'Escape Hatches',
|
|
49
|
+
desc: 'Every convention can be overridden via basicben.config.js. Use what works, change what doesn\'t.'
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const comparisons = [
|
|
54
|
+
{ name: 'Next.js / Remix', issue: 'Too much magic, vendor lock-in' },
|
|
55
|
+
{ name: 'Express + Vite', issue: 'Wire everything yourself' },
|
|
56
|
+
{ name: 'BasicBen', issue: 'Conventions + control', highlight: true }
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const builtIns = [
|
|
60
|
+
'JWT authentication',
|
|
61
|
+
'Password hashing',
|
|
62
|
+
'Request validation',
|
|
63
|
+
'Database migrations',
|
|
64
|
+
'Auto-loading routes',
|
|
65
|
+
'CLI scaffolding'
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="space-y-16 py-8">
|
|
70
|
+
{/* Hero */}
|
|
71
|
+
<section className="text-center">
|
|
72
|
+
<div className="flex justify-center mb-6">
|
|
73
|
+
<Logo className="w-16 h-16" />
|
|
74
|
+
</div>
|
|
75
|
+
<p className={`text-xs ${t.subtle} mb-3 tracking-wide uppercase`}>Full-stack React Framework</p>
|
|
76
|
+
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight mb-4">
|
|
77
|
+
Ship faster with less
|
|
78
|
+
</h1>
|
|
79
|
+
<p className={`${t.muted} max-w-lg mx-auto mb-8 text-lg`}>
|
|
80
|
+
A productive, convention-driven framework for React apps. Zero runtime dependencies. Maximum clarity.
|
|
81
|
+
</p>
|
|
82
|
+
<div className="flex flex-col items-center gap-4">
|
|
83
|
+
<div className={`px-4 py-3 rounded-lg ${t.card} border ${t.border} font-mono text-sm`}>
|
|
84
|
+
<span className={t.muted}>$</span> npx create-basicben-app my-app
|
|
85
|
+
</div>
|
|
86
|
+
<div className="flex gap-3">
|
|
87
|
+
<Button onClick={() => navigate('/docs')}>Get Started</Button>
|
|
88
|
+
<Button variant="secondary" onClick={() => window.open('https://github.com/BasicBenFramework/basicben-framework', '_blank')}>
|
|
89
|
+
GitHub
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
{/* The Problem */}
|
|
96
|
+
<section>
|
|
97
|
+
<h2 className={`text-center text-sm font-medium uppercase tracking-wider ${t.muted} mb-6`}>The Problem</h2>
|
|
98
|
+
<Card>
|
|
99
|
+
<div className="space-y-3">
|
|
100
|
+
{comparisons.map(({ name, issue, highlight }) => (
|
|
101
|
+
<div
|
|
102
|
+
key={name}
|
|
103
|
+
className={`flex items-center justify-between p-3 rounded-lg ${
|
|
104
|
+
highlight
|
|
105
|
+
? `${dark ? 'bg-white/10' : 'bg-black/10'} border ${t.border}`
|
|
106
|
+
: ''
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
<span className={`font-medium ${highlight ? '' : t.muted}`}>{name}</span>
|
|
110
|
+
<span className={`text-sm ${highlight ? (dark ? 'text-green-400' : 'text-green-600') : t.subtle}`}>
|
|
111
|
+
{highlight ? '✓ ' : ''}{issue}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</Card>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
{/* Features */}
|
|
120
|
+
<section>
|
|
121
|
+
<h2 className={`text-center text-sm font-medium uppercase tracking-wider ${t.muted} mb-6`}>Why BasicBen</h2>
|
|
122
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
123
|
+
{features.map(({ icon, title, desc }) => (
|
|
124
|
+
<Card key={title} className="hover:border-opacity-50 transition">
|
|
125
|
+
<div className={`inline-flex p-2 rounded-lg ${t.card} border ${t.border} mb-3`}>
|
|
126
|
+
{icon}
|
|
127
|
+
</div>
|
|
128
|
+
<h3 className="font-semibold mb-1">{title}</h3>
|
|
129
|
+
<p className={`text-sm ${t.muted}`}>{desc}</p>
|
|
130
|
+
</Card>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
</section>
|
|
134
|
+
|
|
135
|
+
{/* Built-in */}
|
|
136
|
+
<section>
|
|
137
|
+
<h2 className={`text-center text-sm font-medium uppercase tracking-wider ${t.muted} mb-6`}>Batteries Included</h2>
|
|
138
|
+
<Card>
|
|
139
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
140
|
+
Everything you need to build a production app, without pulling in a dozen packages.
|
|
141
|
+
</p>
|
|
142
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
|
143
|
+
{builtIns.map(item => (
|
|
144
|
+
<div
|
|
145
|
+
key={item}
|
|
146
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-lg ${t.card} border ${t.border} text-sm`}
|
|
147
|
+
>
|
|
148
|
+
<svg className={`w-4 h-4 flex-shrink-0 ${dark ? 'text-green-400' : 'text-green-600'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
149
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
150
|
+
</svg>
|
|
151
|
+
<span>{item}</span>
|
|
152
|
+
</div>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</Card>
|
|
156
|
+
</section>
|
|
157
|
+
|
|
158
|
+
{/* Code Example */}
|
|
159
|
+
<section>
|
|
160
|
+
<h2 className={`text-center text-sm font-medium uppercase tracking-wider ${t.muted} mb-6`}>Clean & Simple</h2>
|
|
161
|
+
<Card>
|
|
162
|
+
<div className={`rounded-lg p-4 font-mono text-sm ${dark ? 'bg-black/50' : 'bg-black/5'} border ${t.border} overflow-x-auto`}>
|
|
163
|
+
<pre className={t.text}>{`// src/routes/posts.js
|
|
164
|
+
import { PostController } from '../controllers/PostController.js'
|
|
165
|
+
import { auth } from '../middleware/auth.js'
|
|
166
|
+
|
|
167
|
+
export default (router) => {
|
|
168
|
+
router.get('/api/posts', PostController.index)
|
|
169
|
+
router.post('/api/posts', auth, PostController.store)
|
|
170
|
+
router.put('/api/posts/:id', auth, PostController.update)
|
|
171
|
+
router.delete('/api/posts/:id', auth, PostController.destroy)
|
|
172
|
+
}`}</pre>
|
|
173
|
+
</div>
|
|
174
|
+
<p className={`text-sm ${t.muted} mt-3`}>
|
|
175
|
+
Routes are auto-loaded. Controllers are plain objects. Middleware is just a function. No decorators, no magic.
|
|
176
|
+
</p>
|
|
177
|
+
</Card>
|
|
178
|
+
</section>
|
|
179
|
+
|
|
180
|
+
{/* Logged in user card */}
|
|
181
|
+
{user && (
|
|
182
|
+
<Card>
|
|
183
|
+
<div className="flex items-center justify-between">
|
|
184
|
+
<div className="flex items-center gap-3">
|
|
185
|
+
<Avatar name={user.name} />
|
|
186
|
+
<div>
|
|
187
|
+
<p className="font-medium text-sm">{user.name}</p>
|
|
188
|
+
<p className={`text-xs ${t.subtle}`}>{user.email}</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex gap-2">
|
|
192
|
+
<Button variant="secondary" onClick={() => navigate('/posts')} className="text-xs px-3 py-1.5">My Posts</Button>
|
|
193
|
+
<Button variant="secondary" onClick={() => navigate('/profile')} className="text-xs px-3 py-1.5">Profile</Button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</Card>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Footer */}
|
|
200
|
+
<footer className={`text-center text-xs ${t.subtle} space-y-2`}>
|
|
201
|
+
<p>Built with Node.js built-ins. Inspired by Laravel.</p>
|
|
202
|
+
<p>BasicBen v0.1.0</p>
|
|
203
|
+
</footer>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate, useParams } from '@basicbenframework/core/client'
|
|
3
|
+
import { PageHeader } from '../components/PageHeader'
|
|
4
|
+
import { BackLink } from '../components/BackLink'
|
|
5
|
+
import { Input } from '../components/Input'
|
|
6
|
+
import { Textarea } from '../components/Textarea'
|
|
7
|
+
import { Button } from '../components/Button'
|
|
8
|
+
import { Loading } from '../components/Loading'
|
|
9
|
+
import { api } from '../../helpers/api'
|
|
10
|
+
import { useToast } from '../contexts/ToastContext'
|
|
11
|
+
|
|
12
|
+
export function PostForm() {
|
|
13
|
+
const navigate = useNavigate()
|
|
14
|
+
const params = useParams()
|
|
15
|
+
const postId = params.id
|
|
16
|
+
const toast = useToast()
|
|
17
|
+
const [form, setForm] = useState({ title: '', content: '', published: false })
|
|
18
|
+
const [loading, setLoading] = useState(!!postId)
|
|
19
|
+
const [saving, setSaving] = useState(false)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (postId) {
|
|
23
|
+
api(`/api/posts/${postId}`)
|
|
24
|
+
.then(data => setForm({ title: data.post.title, content: data.post.content, published: !!data.post.published }))
|
|
25
|
+
.catch(err => {
|
|
26
|
+
toast.error(err.message)
|
|
27
|
+
navigate('/posts')
|
|
28
|
+
})
|
|
29
|
+
.finally(() => setLoading(false))
|
|
30
|
+
}
|
|
31
|
+
}, [postId])
|
|
32
|
+
|
|
33
|
+
const handleSubmit = async (e) => {
|
|
34
|
+
e.preventDefault()
|
|
35
|
+
setSaving(true)
|
|
36
|
+
try {
|
|
37
|
+
if (postId) {
|
|
38
|
+
await api(`/api/posts/${postId}`, { method: 'PUT', body: JSON.stringify(form) })
|
|
39
|
+
toast.success('Post updated')
|
|
40
|
+
} else {
|
|
41
|
+
await api('/api/posts', { method: 'POST', body: JSON.stringify(form) })
|
|
42
|
+
toast.success('Post created')
|
|
43
|
+
}
|
|
44
|
+
navigate('/posts')
|
|
45
|
+
} catch (err) {
|
|
46
|
+
toast.error(err.message)
|
|
47
|
+
} finally {
|
|
48
|
+
setSaving(false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (loading) return <Loading />
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="max-w-xl mx-auto">
|
|
56
|
+
<BackLink onClick={() => navigate('/posts')}>Back to posts</BackLink>
|
|
57
|
+
<PageHeader title={postId ? 'Edit Post' : 'New Post'} />
|
|
58
|
+
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
|
|
59
|
+
<Input placeholder="Title" required value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} />
|
|
60
|
+
<Textarea placeholder="Write your post content..." required rows={10} value={form.content} onChange={e => setForm({ ...form, content: e.target.value })} />
|
|
61
|
+
<label className="flex items-center gap-2 text-sm">
|
|
62
|
+
<input type="checkbox" checked={form.published} onChange={e => setForm({ ...form, published: e.target.checked })} className="rounded" />
|
|
63
|
+
Publish this post
|
|
64
|
+
</label>
|
|
65
|
+
<Button type="submit" disabled={saving} className="w-full">{saving ? '...' : postId ? 'Update Post' : 'Create Post'}</Button>
|
|
66
|
+
</form>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate } from '@basicbenframework/core/client'
|
|
3
|
+
import { useTheme } from '../components/ThemeContext'
|
|
4
|
+
import { PageHeader } from '../components/PageHeader'
|
|
5
|
+
import { Card } from '../components/Card'
|
|
6
|
+
import { Button } from '../components/Button'
|
|
7
|
+
import { Loading } from '../components/Loading'
|
|
8
|
+
import { Empty } from '../components/Empty'
|
|
9
|
+
import { api } from '../../helpers/api'
|
|
10
|
+
import { useToast } from '../contexts/ToastContext'
|
|
11
|
+
|
|
12
|
+
export function Posts() {
|
|
13
|
+
const navigate = useNavigate()
|
|
14
|
+
const { t } = useTheme()
|
|
15
|
+
const toast = useToast()
|
|
16
|
+
const [posts, setPosts] = useState([])
|
|
17
|
+
const [loading, setLoading] = useState(true)
|
|
18
|
+
|
|
19
|
+
const loadPosts = () => api('/api/posts').then(data => setPosts(data.posts)).finally(() => setLoading(false))
|
|
20
|
+
|
|
21
|
+
useEffect(() => { loadPosts() }, [])
|
|
22
|
+
|
|
23
|
+
const deletePost = async (id) => {
|
|
24
|
+
if (!confirm('Delete this post?')) return
|
|
25
|
+
try {
|
|
26
|
+
await api(`/api/posts/${id}`, { method: 'DELETE' })
|
|
27
|
+
toast.success('Post deleted')
|
|
28
|
+
loadPosts()
|
|
29
|
+
} catch (err) {
|
|
30
|
+
toast.error(err.message)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (loading) return <Loading />
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<PageHeader title="My Posts" action={<Button onClick={() => navigate('/posts/new')}>New Post</Button>} />
|
|
39
|
+
{posts.length === 0 ? (
|
|
40
|
+
<Empty>No posts yet. Create your first one!</Empty>
|
|
41
|
+
) : (
|
|
42
|
+
<div className="space-y-3">
|
|
43
|
+
{posts.map(post => (
|
|
44
|
+
<Card key={post.id} className="flex items-center justify-between">
|
|
45
|
+
<div className="flex-1 min-w-0 mr-4">
|
|
46
|
+
<h2 className="font-medium truncate">{post.title}</h2>
|
|
47
|
+
<p className={`text-xs ${t.subtle}`}>{post.published ? 'Published' : 'Draft'} • {new Date(post.created_at).toLocaleDateString()}</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex gap-2">
|
|
50
|
+
<Button variant="secondary" onClick={() => navigate(`/posts/${post.id}/edit`)} className="text-xs px-3 py-1.5">Edit</Button>
|
|
51
|
+
<Button variant="danger" onClick={() => deletePost(post.id)} className="text-xs px-3 py-1.5">Delete</Button>
|
|
52
|
+
</div>
|
|
53
|
+
</Card>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useAuth } from '@basicbenframework/core/client'
|
|
3
|
+
import { PageHeader } from '../components/PageHeader'
|
|
4
|
+
import { Card } from '../components/Card'
|
|
5
|
+
import { Input } from '../components/Input'
|
|
6
|
+
import { Button } from '../components/Button'
|
|
7
|
+
import { api } from '../../helpers/api'
|
|
8
|
+
import { useToast } from '../contexts/ToastContext'
|
|
9
|
+
|
|
10
|
+
export function Profile() {
|
|
11
|
+
const { user, setUser } = useAuth()
|
|
12
|
+
const toast = useToast()
|
|
13
|
+
const [form, setForm] = useState({ name: user?.name || '', email: user?.email || '' })
|
|
14
|
+
const [pwForm, setPwForm] = useState({ currentPassword: '', newPassword: '' })
|
|
15
|
+
const [loading, setLoading] = useState(false)
|
|
16
|
+
|
|
17
|
+
const updateProfile = async (e) => {
|
|
18
|
+
e.preventDefault()
|
|
19
|
+
setLoading(true)
|
|
20
|
+
try {
|
|
21
|
+
const data = await api('/api/profile', { method: 'PUT', body: JSON.stringify(form) })
|
|
22
|
+
setUser(data.user)
|
|
23
|
+
toast.success('Profile updated')
|
|
24
|
+
} catch (err) {
|
|
25
|
+
toast.error(err.message)
|
|
26
|
+
} finally {
|
|
27
|
+
setLoading(false)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const changePassword = async (e) => {
|
|
32
|
+
e.preventDefault()
|
|
33
|
+
setLoading(true)
|
|
34
|
+
try {
|
|
35
|
+
await api('/api/profile/password', { method: 'PUT', body: JSON.stringify(pwForm) })
|
|
36
|
+
setPwForm({ currentPassword: '', newPassword: '' })
|
|
37
|
+
toast.success('Password changed')
|
|
38
|
+
} catch (err) {
|
|
39
|
+
toast.error(err.message)
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="max-w-md mx-auto space-y-6">
|
|
47
|
+
<PageHeader title="Profile" />
|
|
48
|
+
|
|
49
|
+
<Card>
|
|
50
|
+
<form onSubmit={updateProfile} className="space-y-3">
|
|
51
|
+
<h2 className="font-medium mb-2">Edit Profile</h2>
|
|
52
|
+
<Input placeholder="Name" required value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
|
|
53
|
+
<Input type="email" placeholder="Email" required value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
|
|
54
|
+
<Button type="submit" disabled={loading} className="w-full">{loading ? '...' : 'Save'}</Button>
|
|
55
|
+
</form>
|
|
56
|
+
</Card>
|
|
57
|
+
|
|
58
|
+
<Card>
|
|
59
|
+
<form onSubmit={changePassword} className="space-y-3">
|
|
60
|
+
<h2 className="font-medium mb-2">Change Password</h2>
|
|
61
|
+
<Input type="password" placeholder="Current password" required value={pwForm.currentPassword} onChange={e => setPwForm({ ...pwForm, currentPassword: e.target.value })} />
|
|
62
|
+
<Input type="password" placeholder="New password" required minLength={8} value={pwForm.newPassword} onChange={e => setPwForm({ ...pwForm, newPassword: e.target.value })} />
|
|
63
|
+
<Button type="submit" disabled={loading} className="w-full">{loading ? '...' : 'Change Password'}</Button>
|
|
64
|
+
</form>
|
|
65
|
+
</Card>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useTheme } from '../components/ThemeContext'
|
|
2
|
+
import { Card } from '../components/Card'
|
|
3
|
+
import { PageHeader } from '../components/PageHeader'
|
|
4
|
+
import { AppLayout } from '../layouts/AppLayout'
|
|
5
|
+
import { DocsLayout } from '../layouts/DocsLayout'
|
|
6
|
+
|
|
7
|
+
export function Routing() {
|
|
8
|
+
const { t } = useTheme()
|
|
9
|
+
|
|
10
|
+
const CodeBlock = ({ children, title }) => (
|
|
11
|
+
<div className="mt-4">
|
|
12
|
+
{title && <div className={`text-xs font-medium mb-2 ${t.muted}`}>{title}</div>}
|
|
13
|
+
<div className={`rounded-lg p-4 font-mono text-sm ${t.card} border ${t.border} overflow-x-auto`}>
|
|
14
|
+
<pre className={t.text}>{children}</pre>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<PageHeader
|
|
22
|
+
title="Routing"
|
|
23
|
+
subtitle="API routes, controllers, and middleware"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<div className="space-y-6">
|
|
27
|
+
<Card>
|
|
28
|
+
<h2 className="text-lg font-semibold mb-2">Route Files</h2>
|
|
29
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
30
|
+
Routes are defined in the <code>src/routes/</code> directory. Each file exports route definitions that map HTTP methods to handlers.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<CodeBlock title="src/routes/posts.js">
|
|
34
|
+
{`import { PostController } from '../controllers/PostController.js'
|
|
35
|
+
import { auth } from '../middleware/auth.js'
|
|
36
|
+
|
|
37
|
+
export default [
|
|
38
|
+
{ method: 'GET', path: '/api/posts', handler: PostController.index, middleware: [auth] },
|
|
39
|
+
{ method: 'GET', path: '/api/posts/:id', handler: PostController.show },
|
|
40
|
+
{ method: 'POST', path: '/api/posts', handler: PostController.store, middleware: [auth] },
|
|
41
|
+
{ method: 'PUT', path: '/api/posts/:id', handler: PostController.update, middleware: [auth] },
|
|
42
|
+
{ method: 'DELETE', path: '/api/posts/:id', handler: PostController.destroy, middleware: [auth] },
|
|
43
|
+
]`}
|
|
44
|
+
</CodeBlock>
|
|
45
|
+
|
|
46
|
+
<CodeBlock title="Generate a route file">
|
|
47
|
+
{`npx basicben make:route posts`}
|
|
48
|
+
</CodeBlock>
|
|
49
|
+
</Card>
|
|
50
|
+
|
|
51
|
+
<Card>
|
|
52
|
+
<h2 className="text-lg font-semibold mb-2">Controllers</h2>
|
|
53
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
54
|
+
Controllers handle the business logic for your routes. They receive the request and return a response.
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<CodeBlock title="src/controllers/PostController.js">
|
|
58
|
+
{`import { db } from 'basicben'
|
|
59
|
+
|
|
60
|
+
export const PostController = {
|
|
61
|
+
// GET /api/posts
|
|
62
|
+
index: async (req, res) => {
|
|
63
|
+
const posts = await (await db.table('posts'))
|
|
64
|
+
.where('user_id', req.user.id)
|
|
65
|
+
.orderBy('created_at', 'DESC')
|
|
66
|
+
.get()
|
|
67
|
+
|
|
68
|
+
return res.json({ posts })
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// GET /api/posts/:id
|
|
72
|
+
show: async (req, res) => {
|
|
73
|
+
const post = await (await db.table('posts')).find(req.params.id)
|
|
74
|
+
|
|
75
|
+
if (!post) {
|
|
76
|
+
return res.status(404).json({ error: 'Post not found' })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return res.json({ post })
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// POST /api/posts
|
|
83
|
+
store: async (req, res) => {
|
|
84
|
+
const { title, content } = req.body
|
|
85
|
+
|
|
86
|
+
const result = await (await db.table('posts')).insert({
|
|
87
|
+
title,
|
|
88
|
+
content,
|
|
89
|
+
user_id: req.user.id
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return res.status(201).json({
|
|
93
|
+
id: result.lastInsertRowid
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// PUT /api/posts/:id
|
|
98
|
+
update: async (req, res) => {
|
|
99
|
+
const { title, content } = req.body
|
|
100
|
+
|
|
101
|
+
await (await db.table('posts'))
|
|
102
|
+
.where('id', req.params.id)
|
|
103
|
+
.where('user_id', req.user.id)
|
|
104
|
+
.update({ title, content })
|
|
105
|
+
|
|
106
|
+
return res.json({ success: true })
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// DELETE /api/posts/:id
|
|
110
|
+
destroy: async (req, res) => {
|
|
111
|
+
await (await db.table('posts'))
|
|
112
|
+
.where('id', req.params.id)
|
|
113
|
+
.where('user_id', req.user.id)
|
|
114
|
+
.delete()
|
|
115
|
+
|
|
116
|
+
return res.json({ success: true })
|
|
117
|
+
}
|
|
118
|
+
}`}
|
|
119
|
+
</CodeBlock>
|
|
120
|
+
|
|
121
|
+
<CodeBlock title="Generate a controller">
|
|
122
|
+
{`npx basicben make:controller Post`}
|
|
123
|
+
</CodeBlock>
|
|
124
|
+
</Card>
|
|
125
|
+
|
|
126
|
+
<Card>
|
|
127
|
+
<h2 className="text-lg font-semibold mb-2">Middleware</h2>
|
|
128
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
129
|
+
Middleware runs before your route handler. Use it for authentication, logging, validation, etc.
|
|
130
|
+
</p>
|
|
131
|
+
|
|
132
|
+
<CodeBlock title="src/middleware/auth.js">
|
|
133
|
+
{`import { verifyToken } from 'basicben/auth'
|
|
134
|
+
import { db } from 'basicben'
|
|
135
|
+
|
|
136
|
+
export const auth = async (req, res, next) => {
|
|
137
|
+
const header = req.headers.authorization
|
|
138
|
+
|
|
139
|
+
if (!header?.startsWith('Bearer ')) {
|
|
140
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const token = header.slice(7)
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const payload = await verifyToken(token)
|
|
147
|
+
const user = await (await db.table('users')).find(payload.userId)
|
|
148
|
+
|
|
149
|
+
if (!user) {
|
|
150
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
req.user = user
|
|
154
|
+
next()
|
|
155
|
+
} catch {
|
|
156
|
+
return res.status(401).json({ error: 'Invalid token' })
|
|
157
|
+
}
|
|
158
|
+
}`}
|
|
159
|
+
</CodeBlock>
|
|
160
|
+
|
|
161
|
+
<CodeBlock title="Generate middleware">
|
|
162
|
+
{`npx basicben make:middleware auth`}
|
|
163
|
+
</CodeBlock>
|
|
164
|
+
</Card>
|
|
165
|
+
|
|
166
|
+
<Card>
|
|
167
|
+
<h2 className="text-lg font-semibold mb-2">Route Parameters</h2>
|
|
168
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
169
|
+
Access URL parameters via <code>req.params</code> and query strings via <code>req.query</code>.
|
|
170
|
+
</p>
|
|
171
|
+
|
|
172
|
+
<CodeBlock title="Parameters and query strings">
|
|
173
|
+
{`// Route: /api/posts/:id
|
|
174
|
+
// URL: /api/posts/123?include=author
|
|
175
|
+
|
|
176
|
+
export const show = async (req, res) => {
|
|
177
|
+
const { id } = req.params // { id: '123' }
|
|
178
|
+
const { include } = req.query // { include: 'author' }
|
|
179
|
+
|
|
180
|
+
// ...
|
|
181
|
+
}`}
|
|
182
|
+
</CodeBlock>
|
|
183
|
+
</Card>
|
|
184
|
+
|
|
185
|
+
<Card>
|
|
186
|
+
<h2 className="text-lg font-semibold mb-2">Request Body</h2>
|
|
187
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
188
|
+
JSON request bodies are automatically parsed and available on <code>req.body</code>.
|
|
189
|
+
</p>
|
|
190
|
+
|
|
191
|
+
<CodeBlock title="Accessing request body">
|
|
192
|
+
{`export const store = async (req, res) => {
|
|
193
|
+
const { title, content, published } = req.body
|
|
194
|
+
|
|
195
|
+
// Validate and use the data
|
|
196
|
+
if (!title) {
|
|
197
|
+
return res.status(400).json({ error: 'Title is required' })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ...
|
|
201
|
+
}`}
|
|
202
|
+
</CodeBlock>
|
|
203
|
+
</Card>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|