@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,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-test-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "basicben dev",
|
|
8
|
+
"build": "basicben build",
|
|
9
|
+
"start": "basicben start",
|
|
10
|
+
"test": "basicben test",
|
|
11
|
+
"migrate": "basicben migrate",
|
|
12
|
+
"migrate:rollback": "basicben migrate:rollback",
|
|
13
|
+
"migrate:fresh": "basicben migrate:fresh",
|
|
14
|
+
"migrate:status": "basicben migrate:status",
|
|
15
|
+
"make:migration": "basicben make:migration",
|
|
16
|
+
"make:controller": "basicben make:controller",
|
|
17
|
+
"make:model": "basicben make:model"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@basicbenframework/core": "file:..",
|
|
21
|
+
"react": "^19.2.0",
|
|
22
|
+
"react-dom": "^19.2.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
26
|
+
"vite": "^7.3.1",
|
|
27
|
+
"vitest": "^4.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Users seeder
|
|
3
|
+
* Creates sample users for development/testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db } from 'basicben'
|
|
7
|
+
import { hashPassword } from 'basicben/auth'
|
|
8
|
+
|
|
9
|
+
export async function seed() {
|
|
10
|
+
const password = await hashPassword('password123')
|
|
11
|
+
|
|
12
|
+
// Create admin user
|
|
13
|
+
await (await db.table('users'))
|
|
14
|
+
.insert({
|
|
15
|
+
name: 'Admin User',
|
|
16
|
+
email: 'admin@example.com',
|
|
17
|
+
password
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Create test user
|
|
21
|
+
await (await db.table('users'))
|
|
22
|
+
.insert({
|
|
23
|
+
name: 'Test User',
|
|
24
|
+
email: 'test@example.com',
|
|
25
|
+
password
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
console.log('Seeded 2 users (password: password123)')
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Posts seeder
|
|
3
|
+
* Creates sample blog posts for development/testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db } from 'basicben'
|
|
7
|
+
|
|
8
|
+
export async function seed() {
|
|
9
|
+
// Get the first user (admin)
|
|
10
|
+
const user = await (await db.table('users')).first()
|
|
11
|
+
|
|
12
|
+
if (!user) {
|
|
13
|
+
console.log('No users found. Run users seed first.')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const posts = [
|
|
18
|
+
{
|
|
19
|
+
user_id: user.id,
|
|
20
|
+
title: 'Welcome to BasicBen',
|
|
21
|
+
content: 'This is your first blog post. BasicBen makes it easy to build full-stack React applications with minimal dependencies.',
|
|
22
|
+
published: 1
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
user_id: user.id,
|
|
26
|
+
title: 'Getting Started with Migrations',
|
|
27
|
+
content: 'Migrations help you version control your database schema. Run `basicben make:migration` to create a new migration.',
|
|
28
|
+
published: 1
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
user_id: user.id,
|
|
32
|
+
title: 'Draft Post Example',
|
|
33
|
+
content: 'This is a draft post that is not yet published.',
|
|
34
|
+
published: 0
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for (const post of posts) {
|
|
39
|
+
await (await db.table('posts')).insert(post)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Seeded ${posts.length} posts`)
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function Alert({ type = 'error', children }) {
|
|
2
|
+
const styles = {
|
|
3
|
+
error: 'text-red-500 bg-red-500/10',
|
|
4
|
+
success: 'text-emerald-500 bg-emerald-500/10'
|
|
5
|
+
}
|
|
6
|
+
return (
|
|
7
|
+
<p className={`text-xs p-2 rounded-lg ${styles[type]}`}>
|
|
8
|
+
{children}
|
|
9
|
+
</p>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useTheme } from './ThemeContext'
|
|
2
|
+
|
|
3
|
+
export function Avatar({ name, size = 'md' }) {
|
|
4
|
+
const { dark } = useTheme()
|
|
5
|
+
const sizes = { sm: 'w-6 h-6 text-xs', md: 'w-8 h-8 text-sm' }
|
|
6
|
+
return (
|
|
7
|
+
<div className={`${sizes[size]} rounded-full flex items-center justify-center font-medium ${dark ? 'bg-white text-black' : 'bg-black text-white'}`}>
|
|
8
|
+
{name[0].toUpperCase()}
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useTheme } from './ThemeContext'
|
|
2
|
+
|
|
3
|
+
export function Button({ variant = 'primary', children, className = '', ...props }) {
|
|
4
|
+
const { t } = useTheme()
|
|
5
|
+
const styles = {
|
|
6
|
+
primary: `${t.btn} ${t.btnHover}`,
|
|
7
|
+
secondary: t.btnSecondary,
|
|
8
|
+
danger: 'bg-red-500/10 text-red-500 hover:bg-red-500/20',
|
|
9
|
+
ghost: `${t.muted} hover:opacity-70`
|
|
10
|
+
}
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
className={`text-sm font-medium rounded-full px-4 py-2 transition disabled:opacity-50 ${styles[variant]} ${className}`}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
{children}
|
|
17
|
+
</button>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useTheme } from './ThemeContext'
|
|
2
|
+
|
|
3
|
+
export function Input({ type = 'text', className = '', ...props }) {
|
|
4
|
+
const { t } = useTheme()
|
|
5
|
+
return (
|
|
6
|
+
<input
|
|
7
|
+
type={type}
|
|
8
|
+
className={`w-full px-3 py-2 text-sm rounded-lg ${t.card} border ${t.border} focus:outline-none ${className}`}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function Logo({ className = "w-6 h-6" }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
viewBox="0 0 32 32"
|
|
5
|
+
fill="none"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
className={className}
|
|
8
|
+
>
|
|
9
|
+
{/* Outer rounded square */}
|
|
10
|
+
<rect
|
|
11
|
+
x="2"
|
|
12
|
+
y="2"
|
|
13
|
+
width="28"
|
|
14
|
+
height="28"
|
|
15
|
+
rx="6"
|
|
16
|
+
fill="currentColor"
|
|
17
|
+
fillOpacity="0.1"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeWidth="2"
|
|
20
|
+
/>
|
|
21
|
+
{/* Stylized "B" made of two brackets and a vertical line */}
|
|
22
|
+
<path
|
|
23
|
+
d="M10 8C10 8 8 8 8 10V14C8 16 10 16 10 16C10 16 8 16 8 18V22C8 24 10 24 10 24"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
strokeWidth="2"
|
|
26
|
+
strokeLinecap="round"
|
|
27
|
+
strokeLinejoin="round"
|
|
28
|
+
/>
|
|
29
|
+
<path
|
|
30
|
+
d="M22 8C22 8 24 8 24 10V14C24 16 22 16 22 16C22 16 24 16 24 18V22C24 24 22 24 22 24"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="2"
|
|
33
|
+
strokeLinecap="round"
|
|
34
|
+
strokeLinejoin="round"
|
|
35
|
+
/>
|
|
36
|
+
{/* Center dot */}
|
|
37
|
+
<circle cx="16" cy="16" r="3" fill="currentColor" />
|
|
38
|
+
</svg>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useTheme } from '../ThemeContext'
|
|
2
|
+
|
|
3
|
+
export function DarkModeToggle({ dark, setDark }) {
|
|
4
|
+
const { t } = useTheme()
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<button
|
|
8
|
+
onClick={() => setDark(!dark)}
|
|
9
|
+
className={`p-2 rounded-lg ${t.card} transition`}
|
|
10
|
+
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
11
|
+
>
|
|
12
|
+
{dark ? (
|
|
13
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
14
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
15
|
+
</svg>
|
|
16
|
+
) : (
|
|
17
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
18
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
19
|
+
</svg>
|
|
20
|
+
)}
|
|
21
|
+
</button>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useTheme } from '../ThemeContext'
|
|
2
|
+
import { NavLink } from '../NavLink'
|
|
3
|
+
import { Button } from '../Button'
|
|
4
|
+
import { DarkModeToggle } from './DarkModeToggle'
|
|
5
|
+
|
|
6
|
+
export function DesktopNav({ user, navigate, logout }) {
|
|
7
|
+
const { t, dark, setDark } = useTheme()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="hidden sm:flex items-center gap-2">
|
|
11
|
+
<NavLink onClick={() => navigate('/docs')}>Docs</NavLink>
|
|
12
|
+
|
|
13
|
+
<div className={`w-px h-5 mx-1 ${dark ? 'bg-white/20' : 'bg-black/20'}`} />
|
|
14
|
+
|
|
15
|
+
<DarkModeToggle dark={dark} setDark={setDark} />
|
|
16
|
+
|
|
17
|
+
{user ? (
|
|
18
|
+
<>
|
|
19
|
+
<NavLink onClick={() => navigate('/feed')}>Feed</NavLink>
|
|
20
|
+
<NavLink onClick={() => navigate('/posts')}>My Posts</NavLink>
|
|
21
|
+
<NavLink onClick={() => navigate('/profile')}>Profile</NavLink>
|
|
22
|
+
<Button variant="secondary" onClick={logout} className="px-3 py-1.5">Log out</Button>
|
|
23
|
+
</>
|
|
24
|
+
) : (
|
|
25
|
+
<>
|
|
26
|
+
<NavLink onClick={() => navigate('/login')}>Sign in</NavLink>
|
|
27
|
+
<Button onClick={() => navigate('/register')} className="px-3 py-1.5">Get started</Button>
|
|
28
|
+
</>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useTheme } from '../ThemeContext'
|
|
2
|
+
import { Logo } from '../Logo'
|
|
3
|
+
|
|
4
|
+
export function MobileNav({ user, navigate, onClose, logout }) {
|
|
5
|
+
const { t } = useTheme()
|
|
6
|
+
|
|
7
|
+
const handleNav = (view) => {
|
|
8
|
+
navigate(view)
|
|
9
|
+
onClose()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const handleLogout = () => {
|
|
13
|
+
logout()
|
|
14
|
+
onClose()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={`fixed inset-0 z-50 ${t.bg} ${t.text}`}>
|
|
19
|
+
<div className="flex flex-col h-full">
|
|
20
|
+
<div className={`flex items-center justify-between h-14 px-6 border-b ${t.border}`}>
|
|
21
|
+
<span className="flex items-center gap-2 font-semibold">
|
|
22
|
+
<Logo className="w-5 h-5" />
|
|
23
|
+
<span>BasicBen</span>
|
|
24
|
+
</span>
|
|
25
|
+
<button
|
|
26
|
+
onClick={onClose}
|
|
27
|
+
className={`p-2 rounded-lg ${t.card} transition`}
|
|
28
|
+
aria-label="Close menu"
|
|
29
|
+
>
|
|
30
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
31
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
32
|
+
</svg>
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
37
|
+
<div className="space-y-1">
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => handleNav('/')}
|
|
40
|
+
className={`w-full text-left px-4 py-3 rounded-lg ${t.card} hover:opacity-80 transition`}
|
|
41
|
+
>
|
|
42
|
+
Home
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => handleNav('/docs')}
|
|
46
|
+
className={`w-full text-left px-4 py-3 rounded-lg ${t.card} hover:opacity-80 transition`}
|
|
47
|
+
>
|
|
48
|
+
Docs
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{user ? (
|
|
53
|
+
<>
|
|
54
|
+
<div className={`my-4 border-t ${t.border}`} />
|
|
55
|
+
<p className={`px-4 py-2 text-xs font-medium uppercase tracking-wider ${t.muted}`}>Account</p>
|
|
56
|
+
<div className="space-y-1">
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => handleNav('/feed')}
|
|
59
|
+
className={`w-full text-left px-4 py-3 rounded-lg ${t.card} hover:opacity-80 transition`}
|
|
60
|
+
>
|
|
61
|
+
Feed
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
onClick={() => handleNav('/posts')}
|
|
65
|
+
className={`w-full text-left px-4 py-3 rounded-lg ${t.card} hover:opacity-80 transition`}
|
|
66
|
+
>
|
|
67
|
+
My Posts
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
onClick={() => handleNav('/profile')}
|
|
71
|
+
className={`w-full text-left px-4 py-3 rounded-lg ${t.card} hover:opacity-80 transition`}
|
|
72
|
+
>
|
|
73
|
+
Profile
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div className={`my-4 border-t ${t.border}`} />
|
|
77
|
+
<button
|
|
78
|
+
onClick={handleLogout}
|
|
79
|
+
className={`w-full text-left px-4 py-3 rounded-lg ${t.btnSecondary} transition`}
|
|
80
|
+
>
|
|
81
|
+
Log out
|
|
82
|
+
</button>
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<div className={`my-4 border-t ${t.border}`} />
|
|
87
|
+
<div className="space-y-2">
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => handleNav('/login')}
|
|
90
|
+
className={`w-full px-4 py-3 rounded-lg ${t.btnSecondary} transition`}
|
|
91
|
+
>
|
|
92
|
+
Sign in
|
|
93
|
+
</button>
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => handleNav('/register')}
|
|
96
|
+
className={`w-full px-4 py-3 rounded-lg ${t.btn} ${t.btnHover} transition`}
|
|
97
|
+
>
|
|
98
|
+
Get started
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useTheme } from './ThemeContext'
|
|
2
|
+
|
|
3
|
+
export function NavLink({ onClick, children, className = '' }) {
|
|
4
|
+
const { t } = useTheme()
|
|
5
|
+
return (
|
|
6
|
+
<button onClick={onClick} className={`text-sm ${t.muted} hover:opacity-70 transition ${className}`}>
|
|
7
|
+
{children}
|
|
8
|
+
</button>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useTheme } from './ThemeContext'
|
|
2
|
+
|
|
3
|
+
export function PostCard({ post, onClick, showAuthor = false }) {
|
|
4
|
+
const { t } = useTheme()
|
|
5
|
+
return (
|
|
6
|
+
<button
|
|
7
|
+
onClick={onClick}
|
|
8
|
+
className={`w-full text-left p-4 rounded-xl ${t.card} border ${t.border} hover:border-opacity-50 transition`}
|
|
9
|
+
>
|
|
10
|
+
<h2 className="font-medium mb-1">{post.title}</h2>
|
|
11
|
+
<p className={`text-sm ${t.muted} line-clamp-2`}>{post.content}</p>
|
|
12
|
+
<p className={`text-xs ${t.subtle} mt-2`}>
|
|
13
|
+
{showAuthor && <>By {post.author_name} • </>}
|
|
14
|
+
{post.published !== undefined && <>{post.published ? 'Published' : 'Draft'} • </>}
|
|
15
|
+
{new Date(post.created_at).toLocaleDateString()}
|
|
16
|
+
</p>
|
|
17
|
+
</button>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useTheme } from './ThemeContext'
|
|
2
|
+
|
|
3
|
+
export function Textarea({ rows = 5, className = '', ...props }) {
|
|
4
|
+
const { t } = useTheme()
|
|
5
|
+
return (
|
|
6
|
+
<textarea
|
|
7
|
+
rows={rows}
|
|
8
|
+
className={`w-full px-3 py-2 text-sm rounded-lg ${t.card} border ${t.border} focus:outline-none resize-none ${className}`}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
const AppContext = createContext()
|
|
4
|
+
|
|
5
|
+
export const useApp = () => {
|
|
6
|
+
const context = useContext(AppContext)
|
|
7
|
+
if (!context) throw new Error('useApp must be used within AppProvider')
|
|
8
|
+
return context
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AppProvider({ children, value }) {
|
|
12
|
+
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
|
|
13
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
const ToastContext = createContext()
|
|
4
|
+
|
|
5
|
+
export const useToast = () => {
|
|
6
|
+
const context = useContext(ToastContext)
|
|
7
|
+
if (!context) throw new Error('useToast must be used within ToastProvider')
|
|
8
|
+
return context
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ToastProvider({ children }) {
|
|
12
|
+
const [toasts, setToasts] = useState([])
|
|
13
|
+
|
|
14
|
+
const addToast = useCallback((message, type = 'success', duration = 4000) => {
|
|
15
|
+
const id = Date.now()
|
|
16
|
+
setToasts(prev => [...prev, { id, message, type }])
|
|
17
|
+
if (duration > 0) {
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
setToasts(prev => prev.filter(t => t.id !== id))
|
|
20
|
+
}, duration)
|
|
21
|
+
}
|
|
22
|
+
return id
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
const removeToast = useCallback((id) => {
|
|
26
|
+
setToasts(prev => prev.filter(t => t.id !== id))
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
const toast = {
|
|
30
|
+
success: (message, duration) => addToast(message, 'success', duration),
|
|
31
|
+
error: (message, duration) => addToast(message, 'error', duration),
|
|
32
|
+
info: (message, duration) => addToast(message, 'info', duration),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ToastContext.Provider value={toast}>
|
|
37
|
+
{children}
|
|
38
|
+
<ToastContainer toasts={toasts} removeToast={removeToast} />
|
|
39
|
+
</ToastContext.Provider>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ToastContainer({ toasts, removeToast }) {
|
|
44
|
+
if (toasts.length === 0) return null
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
|
|
48
|
+
{toasts.map(toast => (
|
|
49
|
+
<Toast key={toast.id} {...toast} onClose={() => removeToast(toast.id)} />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Toast({ message, type, onClose }) {
|
|
56
|
+
const styles = {
|
|
57
|
+
success: 'bg-emerald-500 text-white',
|
|
58
|
+
error: 'bg-red-500 text-white',
|
|
59
|
+
info: 'bg-blue-500 text-white',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const icons = {
|
|
63
|
+
success: (
|
|
64
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
65
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
66
|
+
</svg>
|
|
67
|
+
),
|
|
68
|
+
error: (
|
|
69
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
70
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
71
|
+
</svg>
|
|
72
|
+
),
|
|
73
|
+
info: (
|
|
74
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
75
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
76
|
+
</svg>
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
className={`flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg animate-slide-in ${styles[type]}`}
|
|
83
|
+
role="alert"
|
|
84
|
+
>
|
|
85
|
+
{icons[type]}
|
|
86
|
+
<p className="text-sm font-medium flex-1">{message}</p>
|
|
87
|
+
<button onClick={onClose} className="opacity-70 hover:opacity-100 transition">
|
|
88
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
89
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useAuth, useNavigate } from '@basicbenframework/core/client'
|
|
3
|
+
import { useTheme } from '../components/ThemeContext'
|
|
4
|
+
import { RootLayout } from './RootLayout'
|
|
5
|
+
import { DesktopNav } from '../components/Nav/DesktopNav'
|
|
6
|
+
import { MobileNav } from '../components/Nav/MobileNav'
|
|
7
|
+
import { DarkModeToggle } from '../components/Nav/DarkModeToggle'
|
|
8
|
+
import { Logo } from '../components/Logo'
|
|
9
|
+
|
|
10
|
+
function AppLayoutInner({ children }) {
|
|
11
|
+
const { t, dark, setDark } = useTheme()
|
|
12
|
+
const { user, logout } = useAuth()
|
|
13
|
+
const navigate = useNavigate()
|
|
14
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="max-w-3xl mx-auto px-6">
|
|
18
|
+
<nav className={`flex items-center justify-between h-14 border-b ${t.border} relative`}>
|
|
19
|
+
<button onClick={() => navigate('/')} className="flex items-center gap-2 font-semibold hover:opacity-70 transition">
|
|
20
|
+
<Logo className="w-6 h-6" />
|
|
21
|
+
<span>BasicBen</span>
|
|
22
|
+
</button>
|
|
23
|
+
<DesktopNav user={user} navigate={navigate} logout={logout} />
|
|
24
|
+
|
|
25
|
+
{/* Mobile Navigation Trigger */}
|
|
26
|
+
<div className="flex sm:hidden items-center gap-2">
|
|
27
|
+
<DarkModeToggle dark={dark} setDark={setDark} />
|
|
28
|
+
<button
|
|
29
|
+
onClick={() => setMobileMenuOpen(true)}
|
|
30
|
+
className={`p-2 rounded-lg ${t.card} transition`}
|
|
31
|
+
aria-label="Open menu"
|
|
32
|
+
>
|
|
33
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
34
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
35
|
+
</svg>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
</nav>
|
|
39
|
+
|
|
40
|
+
<main className="py-8">{children}</main>
|
|
41
|
+
|
|
42
|
+
{mobileMenuOpen && (
|
|
43
|
+
<MobileNav
|
|
44
|
+
user={user}
|
|
45
|
+
navigate={navigate}
|
|
46
|
+
onClose={() => setMobileMenuOpen(false)}
|
|
47
|
+
logout={logout}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function AppLayout({ children }) {
|
|
55
|
+
return (
|
|
56
|
+
<RootLayout>
|
|
57
|
+
<AppLayoutInner>{children}</AppLayoutInner>
|
|
58
|
+
</RootLayout>
|
|
59
|
+
)
|
|
60
|
+
}
|