devextreme-cli 1.11.0-alpha.0 → 1.11.0-beta.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/package.json +4 -3
- package/src/application.js +40 -20
- package/src/applications/application.angular.js +11 -3
- package/src/applications/application.nextjs.js +231 -0
- package/src/applications/application.react.js +15 -5
- package/src/templates/nextjs/application/.env +1 -0
- package/src/templates/nextjs/application/devextreme.json +63 -0
- package/src/templates/nextjs/application/next.config.mjs +32 -0
- package/src/templates/nextjs/application/public/logo192.png +0 -0
- package/src/templates/nextjs/application/public/logo512.png +0 -0
- package/src/templates/nextjs/application/public/manifest.json +25 -0
- package/src/templates/nextjs/application/public/robots.txt +3 -0
- package/src/templates/nextjs/application/src/app/actions/auth.ts +76 -0
- package/src/templates/nextjs/application/src/app/auth/[type]/page.tsx +49 -0
- package/src/templates/nextjs/application/src/app/layout.tsx +17 -0
- package/src/templates/nextjs/application/src/app/lib/session.ts +47 -0
- package/src/templates/nextjs/application/src/app/pages/layout.tsx +18 -0
- package/src/templates/nextjs/application/src/app-info.tsx +5 -0
- package/src/templates/nextjs/application/src/app-navigation.tsx +21 -0
- package/src/templates/nextjs/application/src/components/change-password-form/ChangePasswordForm.tsx +86 -0
- package/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.scss +19 -0
- package/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.tsx +107 -0
- package/src/templates/nextjs/application/src/components/footer/Footer.scss +12 -0
- package/src/templates/nextjs/application/src/components/footer/Footer.tsx +5 -0
- package/src/templates/nextjs/application/src/components/header/Header.scss +40 -0
- package/src/templates/nextjs/application/src/components/header/Header.tsx +38 -0
- package/src/templates/nextjs/application/src/components/index.tsx +7 -0
- package/src/templates/nextjs/application/src/components/login-form/LoginForm.scss +12 -0
- package/src/templates/nextjs/application/src/components/login-form/LoginForm.tsx +101 -0
- package/src/templates/nextjs/application/src/components/reset-password-form/ResetPasswordForm.scss +12 -0
- package/src/templates/nextjs/application/src/components/reset-password-form/ResetPasswordForm.tsx +78 -0
- package/src/templates/nextjs/application/src/components/side-navigation-menu/SideNavigationMenu.scss +71 -0
- package/src/templates/nextjs/application/src/components/side-navigation-menu/SideNavigationMenu.tsx +88 -0
- package/src/templates/nextjs/application/src/components/theme-switcher/ThemeSwitcher.tsx +21 -0
- package/src/templates/nextjs/application/src/components/user-panel/UserPanel.scss +51 -0
- package/src/templates/nextjs/application/src/components/user-panel/UserPanel.tsx +55 -0
- package/src/templates/nextjs/application/src/dx-styles.scss +106 -0
- package/src/templates/nextjs/application/src/index.css +12 -0
- package/src/templates/nextjs/application/src/layouts/index.tsx +3 -0
- package/src/templates/nextjs/application/src/layouts/side-nav-inner-toolbar/side-nav-inner-toolbar.scss +17 -0
- package/src/templates/nextjs/application/src/layouts/side-nav-inner-toolbar/side-nav-inner-toolbar.tsx +133 -0
- package/src/templates/nextjs/application/src/layouts/side-nav-outer-toolbar/side-nav-outer-toolbar.scss +10 -0
- package/src/templates/nextjs/application/src/layouts/side-nav-outer-toolbar/side-nav-outer-toolbar.tsx +119 -0
- package/src/templates/nextjs/application/src/layouts/single-card/single-card.scss +42 -0
- package/src/templates/nextjs/application/src/layouts/single-card/single-card.tsx +16 -0
- package/src/templates/nextjs/application/src/middleware.ts +46 -0
- package/src/templates/nextjs/application/src/theme.tsx +66 -0
- package/src/templates/nextjs/application/src/themes/metadata.additional.dark.json +11 -0
- package/src/templates/nextjs/application/src/themes/metadata.additional.json +11 -0
- package/src/templates/nextjs/application/src/themes/metadata.base.dark.json +8 -0
- package/src/templates/nextjs/application/src/themes/metadata.base.json +7 -0
- package/src/templates/nextjs/application/src/types.tsx +60 -0
- package/src/templates/nextjs/application/src/utils/default-user.tsx +7 -0
- package/src/templates/nextjs/application/src/utils/media-query.tsx +56 -0
- package/src/templates/nextjs/application/src/variables.scss +53 -0
- package/src/templates/nextjs/page/page.scss +0 -0
- package/src/templates/nextjs/page/page.tsx +13 -0
- package/src/templates/nextjs/sample-pages/home/home.scss +37 -0
- package/src/templates/nextjs/sample-pages/home/page.tsx +101 -0
- package/src/templates/nextjs/sample-pages/profile/page.tsx +61 -0
- package/src/templates/nextjs/sample-pages/profile/profile.scss +19 -0
- package/src/templates/nextjs/sample-pages/tasks/page.tsx +112 -0
- package/src/templates/nextjs/sample-pages/tasks/tasks.scss +3 -0
- package/src/templates/react/application/src/components/header/Header.scss +1 -1
- package/src/templates/react/application/src/dx-styles.scss +3 -3
- package/src/templates/vue-v3/application/src/components/header-toolbar.vue +1 -1
- package/src/templates/vue-v3/application/src/dx-styles.scss +4 -3
- package/src/utility/latest-versions.js +6 -4
- package/src/utility/module.js +11 -3
- package/src/utility/prompts/react-app-type.js +17 -0
- package/src/utility/run-command.js +10 -2
- package/src/utility/template-creator.js +8 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { use } from 'react';
|
|
3
|
+
import { notFound } from 'next/navigation';
|
|
4
|
+
import { SingleCard } from '@/layouts';
|
|
5
|
+
import {
|
|
6
|
+
LoginForm,
|
|
7
|
+
CreateAccountForm,
|
|
8
|
+
ResetPasswordForm,
|
|
9
|
+
ChangePasswordForm,
|
|
10
|
+
} from '@/components';
|
|
11
|
+
|
|
12
|
+
const formText<%=#isTypeScript%>: Record<string, Record<string, string>><%=/isTypeScript%> = {
|
|
13
|
+
'login': {
|
|
14
|
+
title: 'Sign In'
|
|
15
|
+
},
|
|
16
|
+
'create-account': {
|
|
17
|
+
title: 'Sign Up'
|
|
18
|
+
},
|
|
19
|
+
'reset-password': {
|
|
20
|
+
title: 'Reset Password',
|
|
21
|
+
description: 'Please enter the email address that you used to register, and we will send you a link to reset your password via Email.'
|
|
22
|
+
},
|
|
23
|
+
'change-password': {
|
|
24
|
+
title: 'Change Password',
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AuthForm({name}<%=#isTypeScript%>: {name: string}<%=/isTypeScript%>) {
|
|
29
|
+
switch (name) {
|
|
30
|
+
case 'login': return <LoginForm />;
|
|
31
|
+
case 'create-account': return <CreateAccountForm />;
|
|
32
|
+
case 'reset-password': return <ResetPasswordForm />;
|
|
33
|
+
case 'change-password': return <ChangePasswordForm />;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function AuthPage({ params }<%=#isTypeScript%>: {params: Promise<{type: string}>}<%=/isTypeScript%>) {
|
|
38
|
+
const { type } = use(params)
|
|
39
|
+
|
|
40
|
+
if (!formText[type]) {
|
|
41
|
+
notFound();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { title, description } = formText[type];
|
|
45
|
+
|
|
46
|
+
return <SingleCard title={title} description={description}>
|
|
47
|
+
<AuthForm name={type}/>
|
|
48
|
+
</SingleCard>
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%=#isTypeScript%>import type { PropsWithChildren } from 'react';
|
|
2
|
+
<%=/isTypeScript%>import { ThemeProvider } from "@/theme";
|
|
3
|
+
|
|
4
|
+
export default function RootLayout({ children }<%=#isTypeScript%>: PropsWithChildren<object><%=/isTypeScript%>) {
|
|
5
|
+
return (
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<title>NextJs Dx App</title>
|
|
8
|
+
<body className="dx-viewport">
|
|
9
|
+
<ThemeProvider>
|
|
10
|
+
<div className='app'>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
</ThemeProvider>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
<%=#isTypeScript%>import type { SessionPayload } from '@/types';
|
|
5
|
+
<%=/isTypeScript%>
|
|
6
|
+
const secretKey = process.env.SESSION_SECRET;
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const encodedKey = encoder.encode(secretKey);
|
|
9
|
+
|
|
10
|
+
export async function encrypt(payload<%=#isTypeScript%>: SessionPayload<%=/isTypeScript%>) {
|
|
11
|
+
return new SignJWT(payload)
|
|
12
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
13
|
+
.setIssuedAt()
|
|
14
|
+
.setExpirationTime('7d')
|
|
15
|
+
.sign(encodedKey);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function decrypt(session<%=#isTypeScript%>: string | undefined = ''<%=/isTypeScript%>) {
|
|
19
|
+
try {
|
|
20
|
+
const { payload } = await jwtVerify(session, encodedKey, {
|
|
21
|
+
algorithms: ['HS256'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return payload;
|
|
25
|
+
} catch {
|
|
26
|
+
console.log('Failed to verify session');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function createSession(userId<%=#isTypeScript%>: string<%=/isTypeScript%>) {
|
|
31
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
32
|
+
const session = await encrypt({ userId, expiresAt });
|
|
33
|
+
const cookieStore = await cookies();
|
|
34
|
+
|
|
35
|
+
cookieStore.set('session', session, {
|
|
36
|
+
httpOnly: true,
|
|
37
|
+
secure: true,
|
|
38
|
+
expires: expiresAt,
|
|
39
|
+
sameSite: 'lax',
|
|
40
|
+
path: '/',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function deleteSession() {
|
|
45
|
+
const cookieStore = await cookies();
|
|
46
|
+
cookieStore.delete('session');
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%=#isTypeScript%>import type { PropsWithChildren } from 'react';
|
|
2
|
+
<%=/isTypeScript%>import appInfo from '@/app-info';
|
|
3
|
+
import { Footer } from '@/components';
|
|
4
|
+
import { <%=layout%> as SideNavBarLayout } from '@/layouts';
|
|
5
|
+
|
|
6
|
+
export default function Content({children}<%=#isTypeScript%>: PropsWithChildren<object><%=/isTypeScript%>) {
|
|
7
|
+
return (
|
|
8
|
+
<SideNavBarLayout title={appInfo.title}>
|
|
9
|
+
{children}
|
|
10
|
+
<Footer>
|
|
11
|
+
Copyright © 2011-{new Date().getFullYear()} {appInfo.title} Inc.
|
|
12
|
+
<br />
|
|
13
|
+
All trademarks or registered trademarks are property of their
|
|
14
|
+
respective owners.
|
|
15
|
+
</Footer>
|
|
16
|
+
</SideNavBarLayout>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const navigation = [<%=^empty%>
|
|
2
|
+
{
|
|
3
|
+
text: 'Home',
|
|
4
|
+
path: '/pages/home',
|
|
5
|
+
icon: 'home'
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
text: 'Examples',
|
|
9
|
+
icon: 'folder',
|
|
10
|
+
items: [
|
|
11
|
+
{
|
|
12
|
+
text: 'Profile',
|
|
13
|
+
path: '/pages/profile'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
text: 'Tasks',
|
|
17
|
+
path: '/pages/tasks'
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
<%=/empty%>];
|
package/src/templates/nextjs/application/src/components/change-password-form/ChangePasswordForm.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import <%=#isTypeScript%>React, <%=/isTypeScript%>{ useState, useRef, useCallback } from 'react';
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
|
+
import Form, {
|
|
5
|
+
Item,
|
|
6
|
+
Label,
|
|
7
|
+
ButtonItem,
|
|
8
|
+
ButtonOptions,
|
|
9
|
+
RequiredRule,
|
|
10
|
+
CustomRule,
|
|
11
|
+
} from 'devextreme-react/form';
|
|
12
|
+
import LoadIndicator from 'devextreme-react/load-indicator';
|
|
13
|
+
import notify from 'devextreme/ui/notify';
|
|
14
|
+
<%=#isTypeScript%>import { ValidationCallbackData } from 'devextreme-react/common';<%=/isTypeScript%>
|
|
15
|
+
import { changePassword } from '@/app/actions/auth';
|
|
16
|
+
|
|
17
|
+
export default function ChangePasswordForm() {
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const formData = useRef({ password: '' });
|
|
21
|
+
|
|
22
|
+
const onSubmit = useCallback(async (e<%=#isTypeScript%>: React.FormEvent<HTMLFormElement><%=/isTypeScript%>) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
const { password } = formData.current;
|
|
25
|
+
setLoading(true);
|
|
26
|
+
|
|
27
|
+
const result = await changePassword(password);
|
|
28
|
+
setLoading(false);
|
|
29
|
+
|
|
30
|
+
if (result.isOk) {
|
|
31
|
+
router.push('/login');
|
|
32
|
+
} else {
|
|
33
|
+
notify(result.message, 'error', 2000);
|
|
34
|
+
}
|
|
35
|
+
}, [router]);
|
|
36
|
+
|
|
37
|
+
const confirmPassword = useCallback(
|
|
38
|
+
({ value }<%=#isTypeScript%>: ValidationCallbackData<%=/isTypeScript%>) => value === formData.current.password,
|
|
39
|
+
[]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<form onSubmit={onSubmit}>
|
|
44
|
+
<Form formData={formData.current} disabled={loading}>
|
|
45
|
+
<Item
|
|
46
|
+
dataField={'password'}
|
|
47
|
+
editorType={'dxTextBox'}
|
|
48
|
+
editorOptions={passwordEditorOptions}
|
|
49
|
+
>
|
|
50
|
+
<RequiredRule message="Password is required" />
|
|
51
|
+
<Label visible={false} />
|
|
52
|
+
</Item>
|
|
53
|
+
<Item
|
|
54
|
+
dataField={'confirmedPassword'}
|
|
55
|
+
editorType={'dxTextBox'}
|
|
56
|
+
editorOptions={confirmedPasswordEditorOptions}
|
|
57
|
+
>
|
|
58
|
+
<RequiredRule message="Password is required" />
|
|
59
|
+
<CustomRule
|
|
60
|
+
message={'Passwords do not match'}
|
|
61
|
+
validationCallback={confirmPassword}
|
|
62
|
+
/>
|
|
63
|
+
<Label visible={false} />
|
|
64
|
+
</Item>
|
|
65
|
+
<ButtonItem>
|
|
66
|
+
<ButtonOptions
|
|
67
|
+
width={'100%'}
|
|
68
|
+
type={'default'}
|
|
69
|
+
useSubmitBehavior={true}
|
|
70
|
+
>
|
|
71
|
+
<span className="dx-button-text">
|
|
72
|
+
{
|
|
73
|
+
loading
|
|
74
|
+
? <LoadIndicator width={'24px'} height={'24px'} visible={true} />
|
|
75
|
+
: 'Continue'
|
|
76
|
+
}
|
|
77
|
+
</span>
|
|
78
|
+
</ButtonOptions>
|
|
79
|
+
</ButtonItem>
|
|
80
|
+
</Form>
|
|
81
|
+
</form>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const passwordEditorOptions = { stylingMode: 'filled', placeholder: 'Password', mode: 'password' };
|
|
86
|
+
const confirmedPasswordEditorOptions = { stylingMode: 'filled', placeholder: 'Confirm Password', mode: 'password' };
|
package/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.scss
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.create-account-form {
|
|
2
|
+
.policy-info {
|
|
3
|
+
color: var(--base-text-color-alpha-7);
|
|
4
|
+
font-size: 12px;
|
|
5
|
+
font-style: normal;
|
|
6
|
+
|
|
7
|
+
a {
|
|
8
|
+
color: var(--base-text-color-alpha-7);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.login-link {
|
|
13
|
+
color: var(--base-accent);
|
|
14
|
+
font-size: 12px;
|
|
15
|
+
text-align: center;
|
|
16
|
+
padding: 6px 0 32px 0;
|
|
17
|
+
border-bottom: 1px solid var(--border-color);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import <%=#isTypeScript%>React, <%=/isTypeScript%>{ useState, useRef, useCallback } from 'react';
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import Form, {
|
|
6
|
+
Item,
|
|
7
|
+
Label,
|
|
8
|
+
ButtonItem,
|
|
9
|
+
ButtonOptions,
|
|
10
|
+
RequiredRule,
|
|
11
|
+
CustomRule,
|
|
12
|
+
EmailRule
|
|
13
|
+
} from 'devextreme-react/form';
|
|
14
|
+
import notify from 'devextreme/ui/notify';
|
|
15
|
+
import LoadIndicator from 'devextreme-react/load-indicator';
|
|
16
|
+
import { signUp } from '@/app/actions/auth';
|
|
17
|
+
<%=#isTypeScript%>import { ValidationCallbackData } from 'devextreme-react/common';<%=/isTypeScript%>
|
|
18
|
+
import './CreateAccountForm.scss';
|
|
19
|
+
|
|
20
|
+
export default function CreateAccountForm() {
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const formData = useRef({ email: '', password: '' });
|
|
24
|
+
|
|
25
|
+
const onSubmit = useCallback(async (e<%=#isTypeScript%>: React.FormEvent<HTMLFormElement><%=/isTypeScript%>) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
const { email, password } = formData.current;
|
|
28
|
+
setLoading(true);
|
|
29
|
+
|
|
30
|
+
const result = await signUp(email, password);
|
|
31
|
+
setLoading(false);
|
|
32
|
+
|
|
33
|
+
if (result.isOk) {
|
|
34
|
+
router.push('/login');
|
|
35
|
+
} else {
|
|
36
|
+
notify(result.message, 'error', 2000);
|
|
37
|
+
}
|
|
38
|
+
}, [router]);
|
|
39
|
+
|
|
40
|
+
const confirmPassword = useCallback(
|
|
41
|
+
({ value }<%=#isTypeScript%>: ValidationCallbackData<%=/isTypeScript%>) => value === formData.current.password,
|
|
42
|
+
[]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<form className={'create-account-form'} onSubmit={onSubmit}>
|
|
47
|
+
<Form formData={formData.current} disabled={loading}>
|
|
48
|
+
<Item
|
|
49
|
+
dataField={'email'}
|
|
50
|
+
editorType={'dxTextBox'}
|
|
51
|
+
editorOptions={emailEditorOptions}
|
|
52
|
+
>
|
|
53
|
+
<RequiredRule message="Email is required" />
|
|
54
|
+
<EmailRule message="Email is invalid" />
|
|
55
|
+
<Label visible={false} />
|
|
56
|
+
</Item>
|
|
57
|
+
<Item
|
|
58
|
+
dataField={'password'}
|
|
59
|
+
editorType={'dxTextBox'}
|
|
60
|
+
editorOptions={passwordEditorOptions}
|
|
61
|
+
>
|
|
62
|
+
<RequiredRule message="Password is required" />
|
|
63
|
+
<Label visible={false} />
|
|
64
|
+
</Item>
|
|
65
|
+
<Item
|
|
66
|
+
dataField={'confirmedPassword'}
|
|
67
|
+
editorType={'dxTextBox'}
|
|
68
|
+
editorOptions={confirmedPasswordEditorOptions}
|
|
69
|
+
>
|
|
70
|
+
<RequiredRule message="Password is required" />
|
|
71
|
+
<CustomRule
|
|
72
|
+
message={'Passwords do not match'}
|
|
73
|
+
validationCallback={confirmPassword}
|
|
74
|
+
/>
|
|
75
|
+
<Label visible={false} />
|
|
76
|
+
</Item>
|
|
77
|
+
<Item>
|
|
78
|
+
<div className='policy-info'>
|
|
79
|
+
By creating an account, you agree to the <Link href="#">Terms of Service</Link> and <Link href="#">Privacy Policy</Link>
|
|
80
|
+
</div>
|
|
81
|
+
</Item>
|
|
82
|
+
<ButtonItem>
|
|
83
|
+
<ButtonOptions
|
|
84
|
+
width={'100%'}
|
|
85
|
+
type={'default'}
|
|
86
|
+
useSubmitBehavior={true}
|
|
87
|
+
>
|
|
88
|
+
<span className="dx-button-text">
|
|
89
|
+
{
|
|
90
|
+
loading
|
|
91
|
+
? <LoadIndicator width={'24px'} height={'24px'} visible={true} />
|
|
92
|
+
: 'Create a new account'
|
|
93
|
+
}
|
|
94
|
+
</span>
|
|
95
|
+
</ButtonOptions>
|
|
96
|
+
</ButtonItem>
|
|
97
|
+
</Form>
|
|
98
|
+
<div className={'login-link'}>
|
|
99
|
+
Have an account? <Link href={'/login'}>Sign In</Link>
|
|
100
|
+
</div>
|
|
101
|
+
</form>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const emailEditorOptions = { stylingMode: 'filled', placeholder: 'Email', mode: 'email' };
|
|
106
|
+
const passwordEditorOptions = { stylingMode: 'filled', placeholder: 'Password', mode: 'password' };
|
|
107
|
+
const confirmedPasswordEditorOptions = { stylingMode: 'filled', placeholder: 'Confirm Password', mode: 'password' };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@use "../../dx-styles.scss" as *;
|
|
2
|
+
|
|
3
|
+
header {
|
|
4
|
+
background-color: var(--base-bg);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.header-component {
|
|
8
|
+
flex: 0 0 auto;
|
|
9
|
+
z-index: 1;
|
|
10
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.dx-toolbar.header-toolbar .dx-toolbar-items-container .dx-toolbar-after {
|
|
14
|
+
padding: 0 40px;
|
|
15
|
+
|
|
16
|
+
@media (max-width: 599.99px) {
|
|
17
|
+
padding: 0 20px;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.dx-toolbar .dx-toolbar-item.dx-toolbar-button.menu-button {
|
|
22
|
+
width: $side-panel-min-width;
|
|
23
|
+
text-align: center;
|
|
24
|
+
padding: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.header-title .dx-item-content {
|
|
28
|
+
padding: 0;
|
|
29
|
+
margin: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.dx-theme-generic {
|
|
33
|
+
.header-toolbar {
|
|
34
|
+
padding: 10px 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.user-button>.dx-button-content {
|
|
38
|
+
padding: 3px;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Toolbar, { Item } from 'devextreme-react/toolbar';
|
|
2
|
+
import Button from 'devextreme-react/button';
|
|
3
|
+
import UserPanel from '@/components/user-panel/UserPanel';
|
|
4
|
+
import './Header.scss';
|
|
5
|
+
import { ThemeSwitcher } from '@/components/theme-switcher/ThemeSwitcher';
|
|
6
|
+
<%=#isTypeScript%>import type { HeaderProps } from '@/types';<%=/isTypeScript%>
|
|
7
|
+
|
|
8
|
+
const renderMenuItem = () => <UserPanel menuMode='list' />;
|
|
9
|
+
|
|
10
|
+
export default function Header({ menuToggleEnabled, title, toggleMenu }<%=#isTypeScript%>: HeaderProps<%=/isTypeScript%>) {
|
|
11
|
+
return (
|
|
12
|
+
<header className={'header-component'}>
|
|
13
|
+
<Toolbar className={'header-toolbar'}>
|
|
14
|
+
<Item
|
|
15
|
+
visible={menuToggleEnabled}
|
|
16
|
+
location={'before'}
|
|
17
|
+
widget={'dxButton'}
|
|
18
|
+
cssClass={'menu-button'}
|
|
19
|
+
>
|
|
20
|
+
<Button icon="menu" stylingMode="text" onClick={toggleMenu} />
|
|
21
|
+
</Item>
|
|
22
|
+
<Item
|
|
23
|
+
location={'before'}
|
|
24
|
+
cssClass={'header-title'}
|
|
25
|
+
text={title}
|
|
26
|
+
visible={!!title}
|
|
27
|
+
/>
|
|
28
|
+
<Item
|
|
29
|
+
location={'after'}
|
|
30
|
+
>
|
|
31
|
+
<ThemeSwitcher />
|
|
32
|
+
</Item>
|
|
33
|
+
<Item location='after' locateInMenu='auto' menuItemRender={renderMenuItem}>
|
|
34
|
+
<UserPanel menuMode='context' />
|
|
35
|
+
</Item>
|
|
36
|
+
</Toolbar>
|
|
37
|
+
</header>
|
|
38
|
+
)}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Header } from './header/Header';
|
|
2
|
+
export { default as Footer } from './footer/Footer';
|
|
3
|
+
export { default as LoginForm } from './login-form/LoginForm';
|
|
4
|
+
export { default as ResetPasswordForm } from './reset-password-form/ResetPasswordForm';
|
|
5
|
+
export { default as ChangePasswordForm } from './change-password-form/ChangePasswordForm';
|
|
6
|
+
export { default as CreateAccountForm } from './create-account-form/CreateAccountForm';
|
|
7
|
+
export { default as SideNavigationMenu } from './side-navigation-menu/SideNavigationMenu';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import <%=#isTypeScript%>React, <%=/isTypeScript%>{ useState, useRef, useCallback } from 'react';
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import Form, {
|
|
6
|
+
Item,
|
|
7
|
+
Label,
|
|
8
|
+
ButtonItem,
|
|
9
|
+
ButtonOptions,
|
|
10
|
+
RequiredRule,
|
|
11
|
+
EmailRule
|
|
12
|
+
} from 'devextreme-react/form';
|
|
13
|
+
import LoadIndicator from 'devextreme-react/load-indicator';
|
|
14
|
+
import Button from 'devextreme-react/button';
|
|
15
|
+
import notify from 'devextreme/ui/notify';
|
|
16
|
+
import { signIn } from '@/app/actions/auth';
|
|
17
|
+
|
|
18
|
+
import './LoginForm.scss';
|
|
19
|
+
|
|
20
|
+
export default function LoginForm() {
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const formData = useRef({ email: '', password: '' });
|
|
24
|
+
|
|
25
|
+
const onSubmit = useCallback(async (e<%=#isTypeScript%>: React.FormEvent<HTMLFormElement><%=/isTypeScript%>) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
const { email, password } = formData.current;
|
|
28
|
+
setLoading(true);
|
|
29
|
+
|
|
30
|
+
const result = await signIn(email, password);
|
|
31
|
+
if (!result.isOk) {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
notify(result.message, 'error', 2000);
|
|
34
|
+
} else {
|
|
35
|
+
router.push('/');
|
|
36
|
+
}
|
|
37
|
+
}, [router]);
|
|
38
|
+
|
|
39
|
+
const onCreateAccountClick = useCallback(() => {
|
|
40
|
+
router.push('/auth/create-account');
|
|
41
|
+
}, [router]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<form className={'login-form'} onSubmit={onSubmit}>
|
|
45
|
+
<Form formData={formData.current} disabled={loading}>
|
|
46
|
+
<Item
|
|
47
|
+
dataField={'email'}
|
|
48
|
+
editorType={'dxTextBox'}
|
|
49
|
+
editorOptions={emailEditorOptions}
|
|
50
|
+
>
|
|
51
|
+
<RequiredRule message="Email is required" />
|
|
52
|
+
<EmailRule message="Email is invalid" />
|
|
53
|
+
<Label visible={false} />
|
|
54
|
+
</Item>
|
|
55
|
+
<Item
|
|
56
|
+
dataField={'password'}
|
|
57
|
+
editorType={'dxTextBox'}
|
|
58
|
+
editorOptions={passwordEditorOptions}
|
|
59
|
+
>
|
|
60
|
+
<RequiredRule message="Password is required" />
|
|
61
|
+
<Label visible={false} />
|
|
62
|
+
</Item>
|
|
63
|
+
<Item
|
|
64
|
+
dataField={'rememberMe'}
|
|
65
|
+
editorType={'dxCheckBox'}
|
|
66
|
+
editorOptions={rememberMeEditorOptions}
|
|
67
|
+
>
|
|
68
|
+
<Label visible={false} />
|
|
69
|
+
</Item>
|
|
70
|
+
<ButtonItem>
|
|
71
|
+
<ButtonOptions
|
|
72
|
+
width={'100%'}
|
|
73
|
+
type={'default'}
|
|
74
|
+
useSubmitBehavior={true}
|
|
75
|
+
>
|
|
76
|
+
<span className="dx-button-text">
|
|
77
|
+
{
|
|
78
|
+
loading
|
|
79
|
+
? <LoadIndicator width={'24px'} height={'24px'} visible={true} />
|
|
80
|
+
: 'Sign In'
|
|
81
|
+
}
|
|
82
|
+
</span>
|
|
83
|
+
</ButtonOptions>
|
|
84
|
+
</ButtonItem>
|
|
85
|
+
</Form>
|
|
86
|
+
<div className={'link'}>
|
|
87
|
+
<Link href={'/auth/reset-password'}>Forgot password?</Link>
|
|
88
|
+
</div>
|
|
89
|
+
<Button
|
|
90
|
+
text={'Create an account'}
|
|
91
|
+
stylingMode={ 'outlined' }
|
|
92
|
+
width={'100%'}
|
|
93
|
+
onClick={onCreateAccountClick}
|
|
94
|
+
/>
|
|
95
|
+
</form>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const emailEditorOptions = { stylingMode: 'filled', placeholder: 'Email', mode: 'email' };
|
|
100
|
+
const passwordEditorOptions = { stylingMode: 'filled', placeholder: 'Password', mode: 'password' };
|
|
101
|
+
const rememberMeEditorOptions = { text: 'Remember me', elementAttr: { class: 'form-text' } };
|