@basicbenframework/create 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/index.js +205 -0
- package/package.json +30 -0
- package/template/.env.example +24 -0
- package/template/README.md +59 -0
- package/template/basicben.config.js +33 -0
- package/template/index.html +54 -0
- package/template/migrations/001_create_users.js +15 -0
- package/template/migrations/002_create_posts.js +18 -0
- package/template/public/.gitkeep +0 -0
- package/template/seeds/01_users.js +29 -0
- package/template/seeds/02_posts.js +43 -0
- package/template/src/client/components/Alert.jsx +11 -0
- package/template/src/client/components/Avatar.jsx +11 -0
- package/template/src/client/components/BackLink.jsx +10 -0
- package/template/src/client/components/Button.jsx +19 -0
- package/template/src/client/components/Card.jsx +10 -0
- package/template/src/client/components/Empty.jsx +6 -0
- package/template/src/client/components/Input.jsx +12 -0
- package/template/src/client/components/Loading.jsx +6 -0
- package/template/src/client/components/Logo.jsx +40 -0
- package/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/template/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/template/src/client/components/Nav/MobileNav.jsx +107 -0
- package/template/src/client/components/NavLink.jsx +10 -0
- package/template/src/client/components/PageHeader.jsx +8 -0
- package/template/src/client/components/PostCard.jsx +19 -0
- package/template/src/client/components/Textarea.jsx +12 -0
- package/template/src/client/components/ThemeContext.jsx +5 -0
- package/template/src/client/contexts/ToastContext.jsx +94 -0
- package/template/src/client/layouts/AppLayout.jsx +60 -0
- package/template/src/client/layouts/AuthLayout.jsx +33 -0
- package/template/src/client/layouts/DocsLayout.jsx +60 -0
- package/template/src/client/layouts/RootLayout.jsx +25 -0
- package/template/src/client/pages/Auth.jsx +55 -0
- package/template/src/client/pages/Authentication.jsx +236 -0
- package/template/src/client/pages/Database.jsx +426 -0
- package/template/src/client/pages/Feed.jsx +34 -0
- package/template/src/client/pages/FeedPost.jsx +37 -0
- package/template/src/client/pages/GettingStarted.jsx +136 -0
- package/template/src/client/pages/Home.jsx +206 -0
- package/template/src/client/pages/PostForm.jsx +69 -0
- package/template/src/client/pages/Posts.jsx +59 -0
- package/template/src/client/pages/Profile.jsx +68 -0
- package/template/src/client/pages/Routing.jsx +207 -0
- package/template/src/client/pages/Testing.jsx +251 -0
- package/template/src/client/pages/Validation.jsx +210 -0
- package/template/src/controllers/AuthController.js +81 -0
- package/template/src/controllers/HomeController.js +17 -0
- package/template/src/controllers/PostController.js +86 -0
- package/template/src/controllers/ProfileController.js +66 -0
- package/template/src/helpers/api.js +24 -0
- package/template/src/main.jsx +9 -0
- package/template/src/middleware/auth.js +16 -0
- package/template/src/models/Post.js +63 -0
- package/template/src/models/User.js +42 -0
- package/template/src/routes/App.jsx +38 -0
- package/template/src/routes/api/auth.js +7 -0
- package/template/src/routes/api/posts.js +15 -0
- package/template/src/routes/api/profile.js +8 -0
- package/template/src/server/index.js +16 -0
- package/template/vite.config.js +18 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate, useParams } from '@basicbenframework/core/client'
|
|
3
|
+
import { useTheme } from '../components/ThemeContext'
|
|
4
|
+
import { Card } from '../components/Card'
|
|
5
|
+
import { BackLink } from '../components/BackLink'
|
|
6
|
+
import { Loading } from '../components/Loading'
|
|
7
|
+
import { api } from '../../helpers/api'
|
|
8
|
+
|
|
9
|
+
export function FeedPost() {
|
|
10
|
+
const navigate = useNavigate()
|
|
11
|
+
const params = useParams()
|
|
12
|
+
const postId = params.id
|
|
13
|
+
const { t } = useTheme()
|
|
14
|
+
const [post, setPost] = useState(null)
|
|
15
|
+
const [loading, setLoading] = useState(true)
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
api(`/api/feed/${postId}`)
|
|
19
|
+
.then(data => setPost(data.post))
|
|
20
|
+
.catch(() => navigate('/feed'))
|
|
21
|
+
.finally(() => setLoading(false))
|
|
22
|
+
}, [postId])
|
|
23
|
+
|
|
24
|
+
if (loading) return <Loading />
|
|
25
|
+
if (!post) return null
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div>
|
|
29
|
+
<BackLink onClick={() => navigate('/feed')}>Back to feed</BackLink>
|
|
30
|
+
<Card className="p-6">
|
|
31
|
+
<h1 className="text-2xl font-bold mb-2">{post.title}</h1>
|
|
32
|
+
<p className={`text-sm ${t.subtle} mb-6`}>By {post.author_name} • {new Date(post.created_at).toLocaleDateString()}</p>
|
|
33
|
+
<p className="whitespace-pre-wrap">{post.content}</p>
|
|
34
|
+
</Card>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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 GettingStarted() {
|
|
8
|
+
const { t } = useTheme()
|
|
9
|
+
|
|
10
|
+
const devCommands = [
|
|
11
|
+
{ cmd: 'npm run dev', desc: 'Start development server' },
|
|
12
|
+
{ cmd: 'npm run build', desc: 'Build for production' },
|
|
13
|
+
{ cmd: 'npm run build -- --static', desc: 'Build client only (static hosts)' },
|
|
14
|
+
{ cmd: 'npm run start', desc: 'Run production server' },
|
|
15
|
+
{ cmd: 'npm run test', desc: 'Run tests with Vitest' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const makeCommands = [
|
|
19
|
+
{ cmd: 'npm run make:controller', desc: 'Generate a controller' },
|
|
20
|
+
{ cmd: 'npm run make:model', desc: 'Generate a model' },
|
|
21
|
+
{ cmd: 'npm run make:route', desc: 'Generate a route file' },
|
|
22
|
+
{ cmd: 'npm run make:migration', desc: 'Generate a migration' },
|
|
23
|
+
{ cmd: 'npm run make:middleware', desc: 'Generate middleware' },
|
|
24
|
+
{ cmd: 'npm run make:seed', desc: 'Generate a seeder' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const dbCommands = [
|
|
28
|
+
{ cmd: 'npm run migrate', desc: 'Run pending migrations' },
|
|
29
|
+
{ cmd: 'npm run migrate:rollback', desc: 'Roll back last batch' },
|
|
30
|
+
{ cmd: 'npm run migrate:fresh', desc: 'Drop all and re-run' },
|
|
31
|
+
{ cmd: 'npm run migrate:status', desc: 'Show migration status' },
|
|
32
|
+
{ cmd: 'npm run db:seed', desc: 'Run database seeders' },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div>
|
|
37
|
+
<PageHeader title="Getting Started" />
|
|
38
|
+
|
|
39
|
+
<div className="space-y-6">
|
|
40
|
+
<Card>
|
|
41
|
+
<h2 className="text-lg font-semibold mb-4">Quick Start</h2>
|
|
42
|
+
<div className={`rounded-lg p-4 font-mono text-sm ${t.card} border ${t.border} overflow-x-auto`}>
|
|
43
|
+
<div className={t.muted}># Create a new project</div>
|
|
44
|
+
<div>npx @basicbenframework/create my-app</div>
|
|
45
|
+
<div className="mt-2" />
|
|
46
|
+
<div className={t.muted}># Navigate to the project</div>
|
|
47
|
+
<div>cd my-app</div>
|
|
48
|
+
<div className="mt-2" />
|
|
49
|
+
<div className={t.muted}># Start the development server</div>
|
|
50
|
+
<div>npm run dev</div>
|
|
51
|
+
</div>
|
|
52
|
+
</Card>
|
|
53
|
+
|
|
54
|
+
<Card>
|
|
55
|
+
<h2 className="text-lg font-semibold mb-4">Project Structure</h2>
|
|
56
|
+
<div className={`rounded-lg p-4 font-mono text-sm ${t.card} border ${t.border} overflow-x-auto`}>
|
|
57
|
+
<pre className={t.text}>{`my-app/
|
|
58
|
+
├── src/
|
|
59
|
+
│ ├── client/ # React frontend
|
|
60
|
+
│ │ ├── components/ # Reusable components
|
|
61
|
+
│ │ └── pages/ # Page components
|
|
62
|
+
│ ├── routes/ # API route files
|
|
63
|
+
│ ├── controllers/ # Business logic
|
|
64
|
+
│ ├── models/ # Database models
|
|
65
|
+
│ └── middleware/ # Route middleware
|
|
66
|
+
├── migrations/ # Database migrations
|
|
67
|
+
├── public/ # Static assets
|
|
68
|
+
└── basicben.config.js # Framework config`}</pre>
|
|
69
|
+
</div>
|
|
70
|
+
</Card>
|
|
71
|
+
|
|
72
|
+
<Card>
|
|
73
|
+
<h2 className="text-lg font-semibold mb-4">CLI Commands</h2>
|
|
74
|
+
|
|
75
|
+
<h3 className={`text-sm font-medium mb-2 ${t.muted}`}>Development</h3>
|
|
76
|
+
<div className="grid gap-2 sm:grid-cols-2 mb-4">
|
|
77
|
+
{devCommands.map(({ cmd, desc }) => (
|
|
78
|
+
<div key={cmd} className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
79
|
+
<code className="text-sm font-semibold">{cmd}</code>
|
|
80
|
+
<p className={`text-xs mt-1 ${t.muted}`}>{desc}</p>
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<h3 className={`text-sm font-medium mb-2 ${t.muted}`}>Scaffolding</h3>
|
|
86
|
+
<div className="grid gap-2 sm:grid-cols-2 mb-4">
|
|
87
|
+
{makeCommands.map(({ cmd, desc }) => (
|
|
88
|
+
<div key={cmd} className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
89
|
+
<code className="text-sm font-semibold">{cmd}</code>
|
|
90
|
+
<p className={`text-xs mt-1 ${t.muted}`}>{desc}</p>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<h3 className={`text-sm font-medium mb-2 ${t.muted}`}>Database</h3>
|
|
96
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
97
|
+
{dbCommands.map(({ cmd, desc }) => (
|
|
98
|
+
<div key={cmd} className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
99
|
+
<code className="text-sm font-semibold">{cmd}</code>
|
|
100
|
+
<p className={`text-xs mt-1 ${t.muted}`}>{desc}</p>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
</Card>
|
|
105
|
+
|
|
106
|
+
<Card>
|
|
107
|
+
<h2 className="text-lg font-semibold mb-4">Resources</h2>
|
|
108
|
+
<div className="flex flex-wrap gap-3">
|
|
109
|
+
<a
|
|
110
|
+
href="https://github.com/BasicBenFramework/basicben-framework"
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener noreferrer"
|
|
113
|
+
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg ${t.btnSecondary} transition text-sm`}
|
|
114
|
+
>
|
|
115
|
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
116
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
117
|
+
</svg>
|
|
118
|
+
GitHub
|
|
119
|
+
</a>
|
|
120
|
+
<a
|
|
121
|
+
href="https://basicben.com"
|
|
122
|
+
target="_blank"
|
|
123
|
+
rel="noopener noreferrer"
|
|
124
|
+
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg ${t.btnSecondary} transition text-sm`}
|
|
125
|
+
>
|
|
126
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
127
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
128
|
+
</svg>
|
|
129
|
+
Documentation
|
|
130
|
+
</a>
|
|
131
|
+
</div>
|
|
132
|
+
</Card>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -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 @basicbenframework/create 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
|
+
}
|