@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.
Files changed (61) hide show
  1. package/index.js +205 -0
  2. package/package.json +30 -0
  3. package/template/.env.example +24 -0
  4. package/template/README.md +59 -0
  5. package/template/basicben.config.js +33 -0
  6. package/template/index.html +54 -0
  7. package/template/migrations/001_create_users.js +15 -0
  8. package/template/migrations/002_create_posts.js +18 -0
  9. package/template/public/.gitkeep +0 -0
  10. package/template/seeds/01_users.js +29 -0
  11. package/template/seeds/02_posts.js +43 -0
  12. package/template/src/client/components/Alert.jsx +11 -0
  13. package/template/src/client/components/Avatar.jsx +11 -0
  14. package/template/src/client/components/BackLink.jsx +10 -0
  15. package/template/src/client/components/Button.jsx +19 -0
  16. package/template/src/client/components/Card.jsx +10 -0
  17. package/template/src/client/components/Empty.jsx +6 -0
  18. package/template/src/client/components/Input.jsx +12 -0
  19. package/template/src/client/components/Loading.jsx +6 -0
  20. package/template/src/client/components/Logo.jsx +40 -0
  21. package/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  22. package/template/src/client/components/Nav/DesktopNav.jsx +32 -0
  23. package/template/src/client/components/Nav/MobileNav.jsx +107 -0
  24. package/template/src/client/components/NavLink.jsx +10 -0
  25. package/template/src/client/components/PageHeader.jsx +8 -0
  26. package/template/src/client/components/PostCard.jsx +19 -0
  27. package/template/src/client/components/Textarea.jsx +12 -0
  28. package/template/src/client/components/ThemeContext.jsx +5 -0
  29. package/template/src/client/contexts/ToastContext.jsx +94 -0
  30. package/template/src/client/layouts/AppLayout.jsx +60 -0
  31. package/template/src/client/layouts/AuthLayout.jsx +33 -0
  32. package/template/src/client/layouts/DocsLayout.jsx +60 -0
  33. package/template/src/client/layouts/RootLayout.jsx +25 -0
  34. package/template/src/client/pages/Auth.jsx +55 -0
  35. package/template/src/client/pages/Authentication.jsx +236 -0
  36. package/template/src/client/pages/Database.jsx +426 -0
  37. package/template/src/client/pages/Feed.jsx +34 -0
  38. package/template/src/client/pages/FeedPost.jsx +37 -0
  39. package/template/src/client/pages/GettingStarted.jsx +136 -0
  40. package/template/src/client/pages/Home.jsx +206 -0
  41. package/template/src/client/pages/PostForm.jsx +69 -0
  42. package/template/src/client/pages/Posts.jsx +59 -0
  43. package/template/src/client/pages/Profile.jsx +68 -0
  44. package/template/src/client/pages/Routing.jsx +207 -0
  45. package/template/src/client/pages/Testing.jsx +251 -0
  46. package/template/src/client/pages/Validation.jsx +210 -0
  47. package/template/src/controllers/AuthController.js +81 -0
  48. package/template/src/controllers/HomeController.js +17 -0
  49. package/template/src/controllers/PostController.js +86 -0
  50. package/template/src/controllers/ProfileController.js +66 -0
  51. package/template/src/helpers/api.js +24 -0
  52. package/template/src/main.jsx +9 -0
  53. package/template/src/middleware/auth.js +16 -0
  54. package/template/src/models/Post.js +63 -0
  55. package/template/src/models/User.js +42 -0
  56. package/template/src/routes/App.jsx +38 -0
  57. package/template/src/routes/api/auth.js +7 -0
  58. package/template/src/routes/api/posts.js +15 -0
  59. package/template/src/routes/api/profile.js +8 -0
  60. package/template/src/server/index.js +16 -0
  61. package/template/vite.config.js +18 -0
@@ -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,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
+ }
@@ -0,0 +1,33 @@
1
+ import { useNavigate } from '@basicbenframework/core/client'
2
+ import { useTheme } from '../components/ThemeContext'
3
+ import { RootLayout } from './RootLayout'
4
+ import { DarkModeToggle } from '../components/Nav/DarkModeToggle'
5
+ import { Logo } from '../components/Logo'
6
+
7
+ function AuthLayoutInner({ children }) {
8
+ const { t, dark, setDark } = useTheme()
9
+ const navigate = useNavigate()
10
+
11
+ return (
12
+ <div className="max-w-3xl mx-auto px-6">
13
+ <nav className={`flex items-center justify-between h-14 border-b ${t.border}`}>
14
+ <button onClick={() => navigate('/')} className="flex items-center gap-2 font-semibold hover:opacity-70 transition">
15
+ <Logo className="w-6 h-6" />
16
+ <span>BasicBen</span>
17
+ </button>
18
+ <DarkModeToggle dark={dark} setDark={setDark} />
19
+ </nav>
20
+ <main className="py-8 flex items-center justify-center min-h-[calc(100vh-3.5rem)]">
21
+ {children}
22
+ </main>
23
+ </div>
24
+ )
25
+ }
26
+
27
+ export function AuthLayout({ children }) {
28
+ return (
29
+ <RootLayout>
30
+ <AuthLayoutInner>{children}</AuthLayoutInner>
31
+ </RootLayout>
32
+ )
33
+ }
@@ -0,0 +1,60 @@
1
+ import { useNavigate, usePath } from '@basicbenframework/core/client'
2
+ import { useTheme } from '../components/ThemeContext'
3
+ import { AppLayout } from './AppLayout'
4
+
5
+ function SidebarLink({ href, active, children }) {
6
+ const { t } = useTheme()
7
+ const navigate = useNavigate()
8
+
9
+ return (
10
+ <button
11
+ onClick={() => navigate(href)}
12
+ className={`block w-full text-left px-3 py-2 rounded-lg text-sm transition ${
13
+ active ? `${t.card} font-medium` : `${t.muted} hover:opacity-70`
14
+ }`}
15
+ >
16
+ {children}
17
+ </button>
18
+ )
19
+ }
20
+
21
+ function DocsSidebar({ children }) {
22
+ const { t } = useTheme()
23
+ const path = usePath()
24
+
25
+ const docLinks = [
26
+ { href: '/docs', label: 'Getting Started' },
27
+ { href: '/docs/routing', label: 'Routing' },
28
+ { href: '/docs/database', label: 'Database' },
29
+ { href: '/docs/authentication', label: 'Authentication' },
30
+ { href: '/docs/validation', label: 'Validation' },
31
+ { href: '/docs/testing', label: 'Testing' },
32
+ ]
33
+
34
+ return (
35
+ <div className="flex gap-8">
36
+ <aside className="hidden md:block w-48 flex-shrink-0">
37
+ <nav className="sticky top-20 space-y-1">
38
+ {docLinks.map(link => (
39
+ <SidebarLink
40
+ key={link.href}
41
+ href={link.href}
42
+ active={path === link.href}
43
+ >
44
+ {link.label}
45
+ </SidebarLink>
46
+ ))}
47
+ </nav>
48
+ </aside>
49
+ <div className="flex-1 min-w-0">{children}</div>
50
+ </div>
51
+ )
52
+ }
53
+
54
+ export function DocsLayout({ children }) {
55
+ return (
56
+ <AppLayout>
57
+ <DocsSidebar>{children}</DocsSidebar>
58
+ </AppLayout>
59
+ )
60
+ }
@@ -0,0 +1,25 @@
1
+ import { useState } from 'react'
2
+ import { ThemeContext } from '../components/ThemeContext'
3
+ import { ToastProvider } from '../contexts/ToastContext'
4
+
5
+ export function RootLayout({ children }) {
6
+ const [dark, setDark] = useState(true)
7
+
8
+ const t = dark
9
+ ? { bg: 'bg-black', text: 'text-white', muted: 'text-white/50', subtle: 'text-white/30', border: 'border-white/10', card: 'bg-white/5', btn: 'bg-white text-black', btnHover: 'hover:bg-white/90', btnSecondary: 'bg-white/10 hover:bg-white/20' }
10
+ : { bg: 'bg-white', text: 'text-black', muted: 'text-black/50', subtle: 'text-black/30', border: 'border-black/10', card: 'bg-black/5', btn: 'bg-black text-white', btnHover: 'hover:bg-black/90', btnSecondary: 'bg-black/10 hover:bg-black/20' }
11
+
12
+ return (
13
+ <ThemeContext.Provider value={{ t, dark, setDark }}>
14
+ <ToastProvider>
15
+ <div className={`min-h-screen ${t.bg} ${t.text} transition-colors duration-300`}>
16
+ <div className="fixed inset-0 pointer-events-none overflow-hidden">
17
+ <div className={`absolute top-0 right-0 w-[500px] h-[500px] rounded-full blur-[150px] ${dark ? 'bg-purple-500/10' : 'bg-purple-500/5'}`} />
18
+ <div className={`absolute bottom-0 left-0 w-[500px] h-[500px] rounded-full blur-[150px] ${dark ? 'bg-blue-500/10' : 'bg-blue-500/5'}`} />
19
+ </div>
20
+ <div className="relative">{children}</div>
21
+ </div>
22
+ </ToastProvider>
23
+ </ThemeContext.Provider>
24
+ )
25
+ }
@@ -0,0 +1,55 @@
1
+ import { useState } from 'react'
2
+ import { useAuth, useNavigate, usePath } from '@basicbenframework/core/client'
3
+ import { useTheme } from '../components/ThemeContext'
4
+ import { Input } from '../components/Input'
5
+ import { Button } from '../components/Button'
6
+ import { api } from '../../helpers/api'
7
+ import { useToast } from '../contexts/ToastContext'
8
+
9
+ export function Auth() {
10
+ const { setUser } = useAuth()
11
+ const navigate = useNavigate()
12
+ const path = usePath()
13
+ const { t } = useTheme()
14
+ const toast = useToast()
15
+ const [form, setForm] = useState({ name: '', email: '', password: '' })
16
+ const [loading, setLoading] = useState(false)
17
+ const isLogin = path === '/login'
18
+
19
+ const handleSubmit = async (e) => {
20
+ e.preventDefault()
21
+ setLoading(true)
22
+ try {
23
+ const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register'
24
+ const data = await api(endpoint, {
25
+ method: 'POST',
26
+ body: JSON.stringify(isLogin ? { email: form.email, password: form.password } : form)
27
+ })
28
+ localStorage.setItem('token', data.token)
29
+ setUser(data.user)
30
+ toast.success(isLogin ? 'Welcome back!' : 'Account created!')
31
+ navigate('/')
32
+ } catch (err) {
33
+ toast.error(err.message)
34
+ } finally {
35
+ setLoading(false)
36
+ }
37
+ }
38
+
39
+ return (
40
+ <div className="max-w-xs mx-auto py-8">
41
+ <h1 className="text-2xl font-bold text-center mb-1">{isLogin ? 'Welcome back' : 'Create account'}</h1>
42
+ <p className={`text-sm ${t.muted} text-center mb-6`}>{isLogin ? 'Sign in to continue' : 'Get started for free'}</p>
43
+ <form onSubmit={handleSubmit} className="space-y-3 mt-4">
44
+ {!isLogin && <Input placeholder="Name" required value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />}
45
+ <Input type="email" placeholder="Email" required value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
46
+ <Input type="password" placeholder="Password" required minLength={8} value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} />
47
+ <Button type="submit" disabled={loading} className="w-full">{loading ? '...' : isLogin ? 'Sign in' : 'Create account'}</Button>
48
+ </form>
49
+ <p className={`text-xs ${t.muted} text-center mt-4`}>
50
+ {isLogin ? "Don't have an account? " : 'Have an account? '}
51
+ <button onClick={() => navigate(isLogin ? '/register' : '/login')} className="underline hover:no-underline">{isLogin ? 'Sign up' : 'Sign in'}</button>
52
+ </p>
53
+ </div>
54
+ )
55
+ }