@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.
Files changed (186) hide show
  1. package/.github/workflows/publish.yml +35 -0
  2. package/README.md +588 -0
  3. package/bin/cli.js +8 -0
  4. package/create-basicben-app/index.js +205 -0
  5. package/create-basicben-app/package.json +30 -0
  6. package/create-basicben-app/template/.env.example +24 -0
  7. package/create-basicben-app/template/README.md +59 -0
  8. package/create-basicben-app/template/basicben.config.js +33 -0
  9. package/create-basicben-app/template/index.html +54 -0
  10. package/create-basicben-app/template/migrations/001_create_users.js +15 -0
  11. package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
  12. package/create-basicben-app/template/public/.gitkeep +0 -0
  13. package/create-basicben-app/template/seeds/01_users.js +29 -0
  14. package/create-basicben-app/template/seeds/02_posts.js +43 -0
  15. package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
  16. package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
  17. package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
  18. package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
  19. package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
  20. package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
  21. package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
  22. package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
  23. package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
  24. package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  25. package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
  26. package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
  27. package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
  28. package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
  29. package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
  30. package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
  31. package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
  32. package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
  33. package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
  34. package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
  35. package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
  36. package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
  37. package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
  38. package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
  39. package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
  40. package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
  41. package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
  42. package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
  43. package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
  44. package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
  45. package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
  46. package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
  47. package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
  48. package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
  49. package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
  50. package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
  51. package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
  52. package/create-basicben-app/template/src/controllers/PostController.js +86 -0
  53. package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
  54. package/create-basicben-app/template/src/helpers/api.js +24 -0
  55. package/create-basicben-app/template/src/main.jsx +9 -0
  56. package/create-basicben-app/template/src/middleware/auth.js +16 -0
  57. package/create-basicben-app/template/src/models/Post.js +63 -0
  58. package/create-basicben-app/template/src/models/User.js +42 -0
  59. package/create-basicben-app/template/src/routes/App.jsx +38 -0
  60. package/create-basicben-app/template/src/routes/api/auth.js +7 -0
  61. package/create-basicben-app/template/src/routes/api/posts.js +15 -0
  62. package/create-basicben-app/template/src/routes/api/profile.js +8 -0
  63. package/create-basicben-app/template/src/server/index.js +16 -0
  64. package/create-basicben-app/template/vite.config.js +18 -0
  65. package/database.sqlite +0 -0
  66. package/my-test-app/.env.example +24 -0
  67. package/my-test-app/README.md +59 -0
  68. package/my-test-app/basicben.config.js +33 -0
  69. package/my-test-app/database.sqlite-shm +0 -0
  70. package/my-test-app/database.sqlite-wal +0 -0
  71. package/my-test-app/index.html +54 -0
  72. package/my-test-app/migrations/001_create_users.js +15 -0
  73. package/my-test-app/migrations/002_create_posts.js +18 -0
  74. package/my-test-app/package-lock.json +2160 -0
  75. package/my-test-app/package.json +29 -0
  76. package/my-test-app/public/.gitkeep +0 -0
  77. package/my-test-app/seeds/01_users.js +29 -0
  78. package/my-test-app/seeds/02_posts.js +43 -0
  79. package/my-test-app/src/client/components/Alert.jsx +11 -0
  80. package/my-test-app/src/client/components/Avatar.jsx +11 -0
  81. package/my-test-app/src/client/components/BackLink.jsx +10 -0
  82. package/my-test-app/src/client/components/Button.jsx +19 -0
  83. package/my-test-app/src/client/components/Card.jsx +10 -0
  84. package/my-test-app/src/client/components/Empty.jsx +6 -0
  85. package/my-test-app/src/client/components/Input.jsx +12 -0
  86. package/my-test-app/src/client/components/Loading.jsx +6 -0
  87. package/my-test-app/src/client/components/Logo.jsx +40 -0
  88. package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  89. package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
  90. package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
  91. package/my-test-app/src/client/components/NavLink.jsx +10 -0
  92. package/my-test-app/src/client/components/PageHeader.jsx +8 -0
  93. package/my-test-app/src/client/components/PostCard.jsx +19 -0
  94. package/my-test-app/src/client/components/Textarea.jsx +12 -0
  95. package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
  96. package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
  97. package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
  98. package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
  99. package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
  100. package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
  101. package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
  102. package/my-test-app/src/client/pages/Auth.jsx +55 -0
  103. package/my-test-app/src/client/pages/Authentication.jsx +236 -0
  104. package/my-test-app/src/client/pages/Database.jsx +426 -0
  105. package/my-test-app/src/client/pages/Feed.jsx +34 -0
  106. package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
  107. package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
  108. package/my-test-app/src/client/pages/Home.jsx +206 -0
  109. package/my-test-app/src/client/pages/PostForm.jsx +69 -0
  110. package/my-test-app/src/client/pages/Posts.jsx +59 -0
  111. package/my-test-app/src/client/pages/Profile.jsx +68 -0
  112. package/my-test-app/src/client/pages/Routing.jsx +207 -0
  113. package/my-test-app/src/client/pages/Testing.jsx +251 -0
  114. package/my-test-app/src/client/pages/Validation.jsx +210 -0
  115. package/my-test-app/src/controllers/AuthController.js +81 -0
  116. package/my-test-app/src/controllers/HomeController.js +17 -0
  117. package/my-test-app/src/controllers/PostController.js +86 -0
  118. package/my-test-app/src/controllers/ProfileController.js +66 -0
  119. package/my-test-app/src/helpers/api.js +24 -0
  120. package/my-test-app/src/main.jsx +9 -0
  121. package/my-test-app/src/middleware/auth.js +16 -0
  122. package/my-test-app/src/models/Post.js +63 -0
  123. package/my-test-app/src/models/User.js +42 -0
  124. package/my-test-app/src/routes/App.jsx +38 -0
  125. package/my-test-app/src/routes/api/auth.js +7 -0
  126. package/my-test-app/src/routes/api/posts.js +15 -0
  127. package/my-test-app/src/routes/api/profile.js +8 -0
  128. package/my-test-app/src/server/index.js +16 -0
  129. package/my-test-app/vite.config.js +18 -0
  130. package/package.json +61 -0
  131. package/scripts/test-app.sh +59 -0
  132. package/src/auth/jwt.js +195 -0
  133. package/src/auth/password.js +132 -0
  134. package/src/cli/colors.js +31 -0
  135. package/src/cli/dispatcher.js +168 -0
  136. package/src/cli/parser.js +91 -0
  137. package/src/client/context.js +4 -0
  138. package/src/client/hooks.js +50 -0
  139. package/src/client/index.js +3 -0
  140. package/src/client/router.js +184 -0
  141. package/src/commands/build.js +155 -0
  142. package/src/commands/dev.js +206 -0
  143. package/src/commands/help.js +84 -0
  144. package/src/commands/make-controller.js +36 -0
  145. package/src/commands/make-middleware.js +44 -0
  146. package/src/commands/make-migration.js +51 -0
  147. package/src/commands/make-model.js +38 -0
  148. package/src/commands/make-route.js +36 -0
  149. package/src/commands/make-seed.js +38 -0
  150. package/src/commands/migrate-fresh.js +32 -0
  151. package/src/commands/migrate-rollback.js +30 -0
  152. package/src/commands/migrate-status.js +41 -0
  153. package/src/commands/migrate.js +30 -0
  154. package/src/commands/seed.js +47 -0
  155. package/src/commands/start.js +69 -0
  156. package/src/commands/test.js +46 -0
  157. package/src/db/Grammar.js +125 -0
  158. package/src/db/QueryBuilder.js +476 -0
  159. package/src/db/adapters/neon.js +170 -0
  160. package/src/db/adapters/planetscale.js +146 -0
  161. package/src/db/adapters/postgres.js +166 -0
  162. package/src/db/adapters/sqlite.js +125 -0
  163. package/src/db/adapters/turso.js +165 -0
  164. package/src/db/index.js +156 -0
  165. package/src/db/migrator.js +250 -0
  166. package/src/db/seeder.js +124 -0
  167. package/src/index.js +12 -0
  168. package/src/scaffolding/index.js +152 -0
  169. package/src/server/body-parser.js +159 -0
  170. package/src/server/cors.js +63 -0
  171. package/src/server/default-entry.js +13 -0
  172. package/src/server/http.js +221 -0
  173. package/src/server/index.js +168 -0
  174. package/src/server/loader.js +128 -0
  175. package/src/server/router.js +281 -0
  176. package/src/server/static.js +139 -0
  177. package/src/validation/index.js +436 -0
  178. package/src/vite/config.js +49 -0
  179. package/stubs/controller.stub +48 -0
  180. package/stubs/middleware-auth.stub +29 -0
  181. package/stubs/middleware.stub +9 -0
  182. package/stubs/migration.stub +17 -0
  183. package/stubs/model.stub +77 -0
  184. package/stubs/route.stub +13 -0
  185. package/stubs/seed.stub +16 -0
  186. 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,10 @@
1
+ import { useTheme } from './ThemeContext'
2
+
3
+ export function BackLink({ onClick, children }) {
4
+ const { t } = useTheme()
5
+ return (
6
+ <button onClick={onClick} className={`text-sm ${t.muted} mb-4 hover:underline`}>
7
+ &larr; {children}
8
+ </button>
9
+ )
10
+ }
@@ -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,10 @@
1
+ import { useTheme } from './ThemeContext'
2
+
3
+ export function Card({ children, className = '' }) {
4
+ const { t } = useTheme()
5
+ return (
6
+ <div className={`p-4 rounded-xl ${t.card} border ${t.border} ${className}`}>
7
+ {children}
8
+ </div>
9
+ )
10
+ }
@@ -0,0 +1,6 @@
1
+ import { useTheme } from './ThemeContext'
2
+
3
+ export function Empty({ children }) {
4
+ const { t } = useTheme()
5
+ return <p className={`text-center ${t.muted} py-12`}>{children}</p>
6
+ }
@@ -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,6 @@
1
+ import { useTheme } from './ThemeContext'
2
+
3
+ export function Loading() {
4
+ const { t } = useTheme()
5
+ return <div className={`text-center ${t.muted} py-12`}>Loading...</div>
6
+ }
@@ -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,8 @@
1
+ export function PageHeader({ title, action }) {
2
+ return (
3
+ <div className="flex items-center justify-between mb-6">
4
+ <h1 className="text-2xl font-bold">{title}</h1>
5
+ {action}
6
+ </div>
7
+ )
8
+ }
@@ -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} &bull; </>}
14
+ {post.published !== undefined && <>{post.published ? 'Published' : 'Draft'} &bull; </>}
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,5 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export const ThemeContext = createContext()
4
+
5
+ export const useTheme = () => useContext(ThemeContext)
@@ -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
+ }