blacksmith-cli 0.1.6 → 0.1.8
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/dist/index.js +1745 -689
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/templates/frontend/package.json.hbs +4 -4
- package/src/templates/frontend/src/__tests__/test-utils.tsx.hbs +5 -4
- package/src/templates/frontend/src/app.tsx.hbs +13 -9
- package/src/templates/frontend/src/features/auth/adapter.ts.hbs +7 -7
- package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +91 -11
- package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +3 -4
- package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +76 -12
- package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +84 -11
- package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +85 -14
- package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +63 -12
- package/src/templates/frontend/src/features/auth/types.ts.hbs +32 -0
- package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +19 -18
- package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +33 -31
- package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +5 -5
- package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +5 -5
- package/src/templates/frontend/src/pages/home/home.tsx.hbs +48 -52
- package/src/templates/frontend/src/router/auth-guard.tsx.hbs +10 -7
- package/src/templates/frontend/src/router/error-boundary.tsx.hbs +16 -12
- package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +12 -12
- package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +62 -55
- package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +6 -6
- package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +1 -1
- package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +18 -2
- package/src/templates/frontend/src/styles/globals.css.hbs +3 -1
- package/src/templates/frontend/src/theme.ts.hbs +10 -0
- package/src/templates/frontend/tailwind.config.js.hbs +2 -4
- package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +3 -2
- package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +3 -2
- package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +5 -3
- package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +3 -2
- package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +3 -2
- package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blacksmith-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Fullstack Django + React framework — one command, one codebase, one mental model",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"author": "",
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@blacksmith/studio": "file:packages/studio",
|
|
37
38
|
"chalk": "^5.4.1",
|
|
38
39
|
"change-case": "^5.4.4",
|
|
39
40
|
"chokidar": "^5.0.0",
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"openapi-ts": "openapi-ts"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"@
|
|
19
|
-
"@
|
|
20
|
-
"@blacksmith-ui/react": "^0.1.2",
|
|
17
|
+
"@chakra-ui/react": "^2.8.0",
|
|
18
|
+
"@emotion/react": "^11.11.0",
|
|
19
|
+
"@emotion/styled": "^11.11.0",
|
|
21
20
|
"@hey-api/client-fetch": "^0.9.0",
|
|
22
21
|
"@hookform/resolvers": "^5.0.0",
|
|
23
22
|
"@tanstack/react-query": "^5.90.0",
|
|
24
23
|
"@tanstack/react-query-devtools": "^5.90.0",
|
|
25
24
|
"clsx": "^2.1.1",
|
|
25
|
+
"framer-motion": "^11.0.0",
|
|
26
26
|
"lucide-react": "^0.400.0",
|
|
27
27
|
"react": "^19.1.0",
|
|
28
28
|
"react-dom": "^19.1.0",
|
|
@@ -2,7 +2,8 @@ import { type ReactElement, type ReactNode } from 'react'
|
|
|
2
2
|
import { render, type RenderOptions } from '@testing-library/react'
|
|
3
3
|
import userEvent from '@testing-library/user-event'
|
|
4
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
-
import {
|
|
5
|
+
import { ChakraProvider } from '@chakra-ui/react'
|
|
6
|
+
import theme from '@/theme'
|
|
6
7
|
import { MemoryRouter } from 'react-router-dom'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -36,7 +37,7 @@ interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
|
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Custom render that wraps components with all app providers:
|
|
39
|
-
* -
|
|
40
|
+
* - ChakraProvider (with theme for consistent snapshots)
|
|
40
41
|
* - QueryClientProvider (with test-friendly defaults)
|
|
41
42
|
* - MemoryRouter (for components that use routing hooks)
|
|
42
43
|
*
|
|
@@ -56,13 +57,13 @@ export function renderWithProviders(
|
|
|
56
57
|
) {
|
|
57
58
|
function Wrapper({ children }: WrapperProps) {
|
|
58
59
|
return (
|
|
59
|
-
<
|
|
60
|
+
<ChakraProvider theme={theme}>
|
|
60
61
|
<QueryClientProvider client={queryClient}>
|
|
61
62
|
<MemoryRouter initialEntries={routerEntries}>
|
|
62
63
|
{children}
|
|
63
64
|
</MemoryRouter>
|
|
64
65
|
</QueryClientProvider>
|
|
65
|
-
</
|
|
66
|
+
</ChakraProvider>
|
|
66
67
|
)
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
9
9
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
10
10
|
import { RouterProvider } from 'react-router-dom'
|
|
11
|
-
import {
|
|
11
|
+
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'
|
|
12
|
+
import theme from '@/theme'
|
|
12
13
|
import { queryClient } from '@/api/query-client'
|
|
13
14
|
import { AuthProvider } from '@/features/auth/components/auth-provider'
|
|
14
15
|
import { router } from '@/router'
|
|
@@ -18,13 +19,16 @@ import '@/api/client'
|
|
|
18
19
|
|
|
19
20
|
export function App() {
|
|
20
21
|
return (
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
<>
|
|
23
|
+
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
|
24
|
+
<ChakraProvider theme={theme}>
|
|
25
|
+
<QueryClientProvider client={queryClient}>
|
|
26
|
+
<AuthProvider>
|
|
27
|
+
<RouterProvider router={router} />
|
|
28
|
+
</AuthProvider>
|
|
29
|
+
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
|
30
|
+
</QueryClientProvider>
|
|
31
|
+
</ChakraProvider>
|
|
32
|
+
</>
|
|
29
33
|
)
|
|
30
34
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Auth Adapter
|
|
3
3
|
*
|
|
4
|
-
* Connects
|
|
4
|
+
* Connects the auth system to the Django JWT backend.
|
|
5
5
|
* Implements the AuthAdapter interface using the generated API client.
|
|
6
6
|
*
|
|
7
7
|
* Generated by Blacksmith. You own this file — customize as needed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { AuthAdapter, AuthUser,
|
|
10
|
+
import type { AuthAdapter, AuthUser, AuthError, SocialProvider } from './types'
|
|
11
11
|
import { setTokens, getAccessToken, clearTokens } from '@/api/client'
|
|
12
12
|
import { parseApiError } from '@/shared/hooks/api-error'
|
|
13
13
|
|
|
@@ -73,7 +73,7 @@ async function fetchCurrentUser(): Promise<AuthUser> {
|
|
|
73
73
|
return mapDjangoUser(data)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function
|
|
76
|
+
export function createAuthAdapter(): AuthAdapter {
|
|
77
77
|
// Try to restore session on init
|
|
78
78
|
;(async () => {
|
|
79
79
|
try {
|
|
@@ -88,7 +88,7 @@ export function createBlacksmithAuthAdapter(): AuthAdapter {
|
|
|
88
88
|
})()
|
|
89
89
|
|
|
90
90
|
return {
|
|
91
|
-
async signInWithEmail(email: string, password: string)
|
|
91
|
+
async signInWithEmail(email: string, password: string) {
|
|
92
92
|
try {
|
|
93
93
|
const data = await apiFetch('/api/auth/login/', {
|
|
94
94
|
method: 'POST',
|
|
@@ -109,7 +109,7 @@ export function createBlacksmithAuthAdapter(): AuthAdapter {
|
|
|
109
109
|
email: string,
|
|
110
110
|
password: string,
|
|
111
111
|
displayName?: string
|
|
112
|
-
)
|
|
112
|
+
) {
|
|
113
113
|
try {
|
|
114
114
|
const data = await apiFetch('/api/auth/register/', {
|
|
115
115
|
method: 'POST',
|
|
@@ -131,7 +131,7 @@ export function createBlacksmithAuthAdapter(): AuthAdapter {
|
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
|
|
134
|
-
async signInWithSocial(_provider: SocialProvider)
|
|
134
|
+
async signInWithSocial(_provider: SocialProvider) {
|
|
135
135
|
return {
|
|
136
136
|
success: false,
|
|
137
137
|
error: {
|
|
@@ -1,32 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Provider
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides auth state and actions via React context,
|
|
5
|
+
* connected to the Django JWT adapter.
|
|
6
6
|
*
|
|
7
7
|
* Generated by Blacksmith. You own this file — customize as needed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { createBlacksmithAuthAdapter } from '../adapter'
|
|
10
|
+
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
|
12
11
|
import type { ReactNode } from 'react'
|
|
12
|
+
import { createAuthAdapter } from '../adapter'
|
|
13
|
+
import type { AuthUser, AuthError, AuthResult } from '../types'
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
interface AuthContextValue {
|
|
16
|
+
user: AuthUser | null
|
|
17
|
+
loading: boolean
|
|
18
|
+
error: AuthError | null
|
|
19
|
+
signInWithEmail: (email: string, password: string) => Promise<AuthResult>
|
|
20
|
+
signUpWithEmail: (email: string, password: string, displayName?: string) => Promise<AuthResult>
|
|
21
|
+
signOut: () => Promise<void>
|
|
22
|
+
sendPasswordResetEmail: (email: string) => Promise<{ success: boolean; error?: AuthError }>
|
|
23
|
+
confirmPasswordReset: (code: string, newPassword: string) => Promise<{ success: boolean; error?: AuthError }>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const AuthContext = createContext<AuthContextValue | null>(null)
|
|
27
|
+
|
|
28
|
+
const adapter = createAuthAdapter()
|
|
15
29
|
|
|
16
30
|
interface Props {
|
|
17
31
|
children: ReactNode
|
|
18
32
|
}
|
|
19
33
|
|
|
20
34
|
export function AuthProvider({ children }: Props) {
|
|
35
|
+
const [user, setUser] = useState<AuthUser | null>(adapter.getCurrentUser())
|
|
36
|
+
const [loading, setLoading] = useState(true)
|
|
37
|
+
const [error, setError] = useState<AuthError | null>(null)
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const unsubscribe = adapter.onAuthStateChanged((u) => {
|
|
41
|
+
setUser(u)
|
|
42
|
+
setLoading(false)
|
|
43
|
+
})
|
|
44
|
+
return unsubscribe
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
const signInWithEmail = useCallback(async (email: string, password: string) => {
|
|
48
|
+
setError(null)
|
|
49
|
+
const result = await adapter.signInWithEmail(email, password)
|
|
50
|
+
if (!result.success && result.error) {
|
|
51
|
+
setError(result.error)
|
|
52
|
+
}
|
|
53
|
+
return result
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const signUpWithEmail = useCallback(async (email: string, password: string, displayName?: string) => {
|
|
57
|
+
setError(null)
|
|
58
|
+
const result = await adapter.signUpWithEmail(email, password, displayName)
|
|
59
|
+
if (!result.success && result.error) {
|
|
60
|
+
setError(result.error)
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
const signOut = useCallback(async () => {
|
|
66
|
+
setError(null)
|
|
67
|
+
await adapter.signOut()
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
const sendPasswordResetEmail = useCallback(async (email: string) => {
|
|
71
|
+
setError(null)
|
|
72
|
+
const result = await adapter.sendPasswordResetEmail(email)
|
|
73
|
+
if (!result.success && result.error) {
|
|
74
|
+
setError(result.error)
|
|
75
|
+
}
|
|
76
|
+
return result
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const confirmPasswordReset = useCallback(async (code: string, newPassword: string) => {
|
|
80
|
+
setError(null)
|
|
81
|
+
const result = await adapter.confirmPasswordReset(code, newPassword)
|
|
82
|
+
if (!result.success && result.error) {
|
|
83
|
+
setError(result.error)
|
|
84
|
+
}
|
|
85
|
+
return result
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
21
88
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
89
|
+
<AuthContext.Provider
|
|
90
|
+
value=\{{
|
|
91
|
+
user,
|
|
92
|
+
loading,
|
|
93
|
+
error,
|
|
94
|
+
signInWithEmail,
|
|
95
|
+
signUpWithEmail,
|
|
96
|
+
signOut,
|
|
97
|
+
sendPasswordResetEmail,
|
|
98
|
+
confirmPasswordReset,
|
|
27
99
|
}}
|
|
28
100
|
>
|
|
29
101
|
{children}
|
|
30
|
-
</
|
|
102
|
+
</AuthContext.Provider>
|
|
31
103
|
)
|
|
32
104
|
}
|
|
105
|
+
|
|
106
|
+
export function useAuthContext() {
|
|
107
|
+
const context = useContext(AuthContext)
|
|
108
|
+
if (!context) {
|
|
109
|
+
throw new Error('useAuthContext must be used within an AuthProvider')
|
|
110
|
+
}
|
|
111
|
+
return context
|
|
112
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Hook
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Provides a convenient interface to the auth context.
|
|
5
5
|
* Import from here so your app has a single auth import path.
|
|
6
6
|
*
|
|
7
7
|
* Generated by Blacksmith. You own this file — customize as needed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { useAuthContext } from '../components/auth-provider'
|
|
11
11
|
|
|
12
12
|
export function useAuth() {
|
|
13
|
-
const auth =
|
|
13
|
+
const auth = useAuthContext()
|
|
14
14
|
|
|
15
15
|
return {
|
|
16
16
|
user: auth.user,
|
|
@@ -22,6 +22,5 @@ export function useAuth() {
|
|
|
22
22
|
logout: auth.signOut,
|
|
23
23
|
sendPasswordResetEmail: auth.sendPasswordResetEmail,
|
|
24
24
|
confirmPasswordReset: auth.confirmPasswordReset,
|
|
25
|
-
socialProviders: auth.socialProviders,
|
|
26
25
|
}
|
|
27
26
|
}
|
|
@@ -1,37 +1,101 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forgot Password Page
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Password reset request form built with Chakra UI.
|
|
5
5
|
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import {
|
|
10
|
+
Box,
|
|
11
|
+
Button,
|
|
12
|
+
FormControl,
|
|
13
|
+
FormLabel,
|
|
14
|
+
Input,
|
|
15
|
+
VStack,
|
|
16
|
+
Heading,
|
|
17
|
+
Text,
|
|
18
|
+
Alert,
|
|
19
|
+
AlertIcon,
|
|
20
|
+
AlertDescription,
|
|
21
|
+
Link as ChakraLink,
|
|
22
|
+
} from '@chakra-ui/react'
|
|
9
23
|
import { useNavigate } from 'react-router-dom'
|
|
10
24
|
import { useAuth } from '../hooks/use-auth'
|
|
11
|
-
import { useState } from 'react'
|
|
12
25
|
import { Path } from '@/router/paths'
|
|
13
26
|
|
|
14
27
|
export default function ForgotPasswordPage() {
|
|
15
28
|
const navigate = useNavigate()
|
|
16
29
|
const { sendPasswordResetEmail, error } = useAuth()
|
|
30
|
+
|
|
31
|
+
const [email, setEmail] = useState('')
|
|
17
32
|
const [loading, setLoading] = useState(false)
|
|
33
|
+
const [sent, setSent] = useState(false)
|
|
18
34
|
|
|
19
|
-
const handleSubmit = async (
|
|
35
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
36
|
+
e.preventDefault()
|
|
20
37
|
setLoading(true)
|
|
21
38
|
try {
|
|
22
|
-
await sendPasswordResetEmail(
|
|
23
|
-
|
|
39
|
+
await sendPasswordResetEmail(email)
|
|
40
|
+
setSent(true)
|
|
24
41
|
} finally {
|
|
25
42
|
setLoading(false)
|
|
26
43
|
}
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
<Box>
|
|
48
|
+
<VStack spacing={2} mb={6} align="start">
|
|
49
|
+
<Heading size="lg">Forgot Password</Heading>
|
|
50
|
+
<Text color="gray.500">
|
|
51
|
+
Enter your email and we'll send you a reset link.
|
|
52
|
+
</Text>
|
|
53
|
+
</VStack>
|
|
54
|
+
|
|
55
|
+
{error && (
|
|
56
|
+
<Alert status="error" borderRadius="md" mb={4}>
|
|
57
|
+
<AlertIcon />
|
|
58
|
+
<AlertDescription>{error.message}</AlertDescription>
|
|
59
|
+
</Alert>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{sent && (
|
|
63
|
+
<Alert status="success" borderRadius="md" mb={4}>
|
|
64
|
+
<AlertIcon />
|
|
65
|
+
<AlertDescription>
|
|
66
|
+
If an account with that email exists, a reset link has been sent.
|
|
67
|
+
</AlertDescription>
|
|
68
|
+
</Alert>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
<form onSubmit={handleSubmit}>
|
|
72
|
+
<VStack spacing={4}>
|
|
73
|
+
<FormControl isRequired>
|
|
74
|
+
<FormLabel>Email</FormLabel>
|
|
75
|
+
<Input
|
|
76
|
+
type="email"
|
|
77
|
+
value={email}
|
|
78
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
79
|
+
placeholder="you@example.com"
|
|
80
|
+
/>
|
|
81
|
+
</FormControl>
|
|
82
|
+
|
|
83
|
+
<Button
|
|
84
|
+
type="submit"
|
|
85
|
+
colorScheme="blue"
|
|
86
|
+
width="full"
|
|
87
|
+
isLoading={loading}
|
|
88
|
+
>
|
|
89
|
+
Send Reset Link
|
|
90
|
+
</Button>
|
|
91
|
+
</VStack>
|
|
92
|
+
</form>
|
|
93
|
+
|
|
94
|
+
<Text fontSize="sm" color="gray.500" mt={6} textAlign="center">
|
|
95
|
+
<ChakraLink color="blue.500" onClick={() => navigate(Path.Login)}>
|
|
96
|
+
Back to Sign In
|
|
97
|
+
</ChakraLink>
|
|
98
|
+
</Text>
|
|
99
|
+
</Box>
|
|
36
100
|
)
|
|
37
101
|
}
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Login Page
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Login form built with Chakra UI, connected to Django JWT backend.
|
|
5
5
|
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import {
|
|
10
|
+
Box,
|
|
11
|
+
Button,
|
|
12
|
+
FormControl,
|
|
13
|
+
FormLabel,
|
|
14
|
+
Input,
|
|
15
|
+
VStack,
|
|
16
|
+
Heading,
|
|
17
|
+
Text,
|
|
18
|
+
Alert,
|
|
19
|
+
AlertIcon,
|
|
20
|
+
AlertDescription,
|
|
21
|
+
Link as ChakraLink,
|
|
22
|
+
} from '@chakra-ui/react'
|
|
9
23
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
10
24
|
import { useAuth } from '../hooks/use-auth'
|
|
11
25
|
import { Path } from '@/router/paths'
|
|
@@ -15,22 +29,81 @@ export default function LoginPage() {
|
|
|
15
29
|
const [searchParams] = useSearchParams()
|
|
16
30
|
const { login, error, isLoading } = useAuth()
|
|
17
31
|
|
|
32
|
+
const [email, setEmail] = useState('')
|
|
33
|
+
const [password, setPassword] = useState('')
|
|
34
|
+
|
|
18
35
|
const redirectTo = searchParams.get('redirect') || Path.Home
|
|
19
36
|
|
|
20
|
-
const handleSubmit = async (
|
|
21
|
-
|
|
37
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
const result = await login(email, password)
|
|
22
40
|
if (result.success) {
|
|
23
41
|
navigate(redirectTo, { replace: true })
|
|
24
42
|
}
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
return (
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
<Box>
|
|
47
|
+
<VStack spacing={2} mb={6} align="start">
|
|
48
|
+
<Heading size="lg">Sign In</Heading>
|
|
49
|
+
<Text color="gray.500">Enter your credentials to access your account.</Text>
|
|
50
|
+
</VStack>
|
|
51
|
+
|
|
52
|
+
{error && (
|
|
53
|
+
<Alert status="error" borderRadius="md" mb={4}>
|
|
54
|
+
<AlertIcon />
|
|
55
|
+
<AlertDescription>{error.message}</AlertDescription>
|
|
56
|
+
</Alert>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<form onSubmit={handleSubmit}>
|
|
60
|
+
<VStack spacing={4}>
|
|
61
|
+
<FormControl isRequired>
|
|
62
|
+
<FormLabel>Email</FormLabel>
|
|
63
|
+
<Input
|
|
64
|
+
type="email"
|
|
65
|
+
value={email}
|
|
66
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
67
|
+
placeholder="you@example.com"
|
|
68
|
+
/>
|
|
69
|
+
</FormControl>
|
|
70
|
+
|
|
71
|
+
<FormControl isRequired>
|
|
72
|
+
<FormLabel>Password</FormLabel>
|
|
73
|
+
<Input
|
|
74
|
+
type="password"
|
|
75
|
+
value={password}
|
|
76
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
77
|
+
placeholder="Enter your password"
|
|
78
|
+
/>
|
|
79
|
+
</FormControl>
|
|
80
|
+
|
|
81
|
+
<Button
|
|
82
|
+
type="submit"
|
|
83
|
+
colorScheme="blue"
|
|
84
|
+
width="full"
|
|
85
|
+
isLoading={isLoading}
|
|
86
|
+
>
|
|
87
|
+
Sign In
|
|
88
|
+
</Button>
|
|
89
|
+
</VStack>
|
|
90
|
+
</form>
|
|
91
|
+
|
|
92
|
+
<VStack spacing={2} mt={6}>
|
|
93
|
+
<ChakraLink
|
|
94
|
+
color="blue.500"
|
|
95
|
+
fontSize="sm"
|
|
96
|
+
onClick={() => navigate(Path.ForgotPassword)}
|
|
97
|
+
>
|
|
98
|
+
Forgot your password?
|
|
99
|
+
</ChakraLink>
|
|
100
|
+
<Text fontSize="sm" color="gray.500">
|
|
101
|
+
Don't have an account?{' '}
|
|
102
|
+
<ChakraLink color="blue.500" onClick={() => navigate(Path.Register)}>
|
|
103
|
+
Sign Up
|
|
104
|
+
</ChakraLink>
|
|
105
|
+
</Text>
|
|
106
|
+
</VStack>
|
|
107
|
+
</Box>
|
|
35
108
|
)
|
|
36
109
|
}
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Register Page
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Registration form built with Chakra UI, connected to Django JWT backend.
|
|
5
5
|
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import {
|
|
10
|
+
Box,
|
|
11
|
+
Button,
|
|
12
|
+
FormControl,
|
|
13
|
+
FormLabel,
|
|
14
|
+
Input,
|
|
15
|
+
VStack,
|
|
16
|
+
Heading,
|
|
17
|
+
Text,
|
|
18
|
+
Alert,
|
|
19
|
+
AlertIcon,
|
|
20
|
+
AlertDescription,
|
|
21
|
+
Link as ChakraLink,
|
|
22
|
+
} from '@chakra-ui/react'
|
|
9
23
|
import { useNavigate } from 'react-router-dom'
|
|
10
24
|
import { useAuth } from '../hooks/use-auth'
|
|
11
25
|
import { Path } from '@/router/paths'
|
|
@@ -14,23 +28,80 @@ export default function RegisterPage() {
|
|
|
14
28
|
const navigate = useNavigate()
|
|
15
29
|
const { register, error, isLoading } = useAuth()
|
|
16
30
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
const [displayName, setDisplayName] = useState('')
|
|
32
|
+
const [email, setEmail] = useState('')
|
|
33
|
+
const [password, setPassword] = useState('')
|
|
34
|
+
|
|
35
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
const result = await register(email, password, displayName)
|
|
23
38
|
if (result.success) {
|
|
24
39
|
navigate(Path.Home, { replace: true })
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
return (
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
<Box>
|
|
45
|
+
<VStack spacing={2} mb={6} align="start">
|
|
46
|
+
<Heading size="lg">Create Account</Heading>
|
|
47
|
+
<Text color="gray.500">Sign up to get started.</Text>
|
|
48
|
+
</VStack>
|
|
49
|
+
|
|
50
|
+
{error && (
|
|
51
|
+
<Alert status="error" borderRadius="md" mb={4}>
|
|
52
|
+
<AlertIcon />
|
|
53
|
+
<AlertDescription>{error.message}</AlertDescription>
|
|
54
|
+
</Alert>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
<form onSubmit={handleSubmit}>
|
|
58
|
+
<VStack spacing={4}>
|
|
59
|
+
<FormControl>
|
|
60
|
+
<FormLabel>Name</FormLabel>
|
|
61
|
+
<Input
|
|
62
|
+
value={displayName}
|
|
63
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
64
|
+
placeholder="Your name"
|
|
65
|
+
/>
|
|
66
|
+
</FormControl>
|
|
67
|
+
|
|
68
|
+
<FormControl isRequired>
|
|
69
|
+
<FormLabel>Email</FormLabel>
|
|
70
|
+
<Input
|
|
71
|
+
type="email"
|
|
72
|
+
value={email}
|
|
73
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
74
|
+
placeholder="you@example.com"
|
|
75
|
+
/>
|
|
76
|
+
</FormControl>
|
|
77
|
+
|
|
78
|
+
<FormControl isRequired>
|
|
79
|
+
<FormLabel>Password</FormLabel>
|
|
80
|
+
<Input
|
|
81
|
+
type="password"
|
|
82
|
+
value={password}
|
|
83
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
84
|
+
placeholder="Choose a password"
|
|
85
|
+
/>
|
|
86
|
+
</FormControl>
|
|
87
|
+
|
|
88
|
+
<Button
|
|
89
|
+
type="submit"
|
|
90
|
+
colorScheme="blue"
|
|
91
|
+
width="full"
|
|
92
|
+
isLoading={isLoading}
|
|
93
|
+
>
|
|
94
|
+
Create Account
|
|
95
|
+
</Button>
|
|
96
|
+
</VStack>
|
|
97
|
+
</form>
|
|
98
|
+
|
|
99
|
+
<Text fontSize="sm" color="gray.500" mt={6} textAlign="center">
|
|
100
|
+
Already have an account?{' '}
|
|
101
|
+
<ChakraLink color="blue.500" onClick={() => navigate(Path.Login)}>
|
|
102
|
+
Sign In
|
|
103
|
+
</ChakraLink>
|
|
104
|
+
</Text>
|
|
105
|
+
</Box>
|
|
35
106
|
)
|
|
36
107
|
}
|