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.
Files changed (72) hide show
  1. package/package.json +4 -3
  2. package/src/application.js +40 -20
  3. package/src/applications/application.angular.js +11 -3
  4. package/src/applications/application.nextjs.js +231 -0
  5. package/src/applications/application.react.js +15 -5
  6. package/src/templates/nextjs/application/.env +1 -0
  7. package/src/templates/nextjs/application/devextreme.json +63 -0
  8. package/src/templates/nextjs/application/next.config.mjs +32 -0
  9. package/src/templates/nextjs/application/public/logo192.png +0 -0
  10. package/src/templates/nextjs/application/public/logo512.png +0 -0
  11. package/src/templates/nextjs/application/public/manifest.json +25 -0
  12. package/src/templates/nextjs/application/public/robots.txt +3 -0
  13. package/src/templates/nextjs/application/src/app/actions/auth.ts +76 -0
  14. package/src/templates/nextjs/application/src/app/auth/[type]/page.tsx +49 -0
  15. package/src/templates/nextjs/application/src/app/layout.tsx +17 -0
  16. package/src/templates/nextjs/application/src/app/lib/session.ts +47 -0
  17. package/src/templates/nextjs/application/src/app/pages/layout.tsx +18 -0
  18. package/src/templates/nextjs/application/src/app-info.tsx +5 -0
  19. package/src/templates/nextjs/application/src/app-navigation.tsx +21 -0
  20. package/src/templates/nextjs/application/src/components/change-password-form/ChangePasswordForm.tsx +86 -0
  21. package/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.scss +19 -0
  22. package/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.tsx +107 -0
  23. package/src/templates/nextjs/application/src/components/footer/Footer.scss +12 -0
  24. package/src/templates/nextjs/application/src/components/footer/Footer.tsx +5 -0
  25. package/src/templates/nextjs/application/src/components/header/Header.scss +40 -0
  26. package/src/templates/nextjs/application/src/components/header/Header.tsx +38 -0
  27. package/src/templates/nextjs/application/src/components/index.tsx +7 -0
  28. package/src/templates/nextjs/application/src/components/login-form/LoginForm.scss +12 -0
  29. package/src/templates/nextjs/application/src/components/login-form/LoginForm.tsx +101 -0
  30. package/src/templates/nextjs/application/src/components/reset-password-form/ResetPasswordForm.scss +12 -0
  31. package/src/templates/nextjs/application/src/components/reset-password-form/ResetPasswordForm.tsx +78 -0
  32. package/src/templates/nextjs/application/src/components/side-navigation-menu/SideNavigationMenu.scss +71 -0
  33. package/src/templates/nextjs/application/src/components/side-navigation-menu/SideNavigationMenu.tsx +88 -0
  34. package/src/templates/nextjs/application/src/components/theme-switcher/ThemeSwitcher.tsx +21 -0
  35. package/src/templates/nextjs/application/src/components/user-panel/UserPanel.scss +51 -0
  36. package/src/templates/nextjs/application/src/components/user-panel/UserPanel.tsx +55 -0
  37. package/src/templates/nextjs/application/src/dx-styles.scss +106 -0
  38. package/src/templates/nextjs/application/src/index.css +12 -0
  39. package/src/templates/nextjs/application/src/layouts/index.tsx +3 -0
  40. package/src/templates/nextjs/application/src/layouts/side-nav-inner-toolbar/side-nav-inner-toolbar.scss +17 -0
  41. package/src/templates/nextjs/application/src/layouts/side-nav-inner-toolbar/side-nav-inner-toolbar.tsx +133 -0
  42. package/src/templates/nextjs/application/src/layouts/side-nav-outer-toolbar/side-nav-outer-toolbar.scss +10 -0
  43. package/src/templates/nextjs/application/src/layouts/side-nav-outer-toolbar/side-nav-outer-toolbar.tsx +119 -0
  44. package/src/templates/nextjs/application/src/layouts/single-card/single-card.scss +42 -0
  45. package/src/templates/nextjs/application/src/layouts/single-card/single-card.tsx +16 -0
  46. package/src/templates/nextjs/application/src/middleware.ts +46 -0
  47. package/src/templates/nextjs/application/src/theme.tsx +66 -0
  48. package/src/templates/nextjs/application/src/themes/metadata.additional.dark.json +11 -0
  49. package/src/templates/nextjs/application/src/themes/metadata.additional.json +11 -0
  50. package/src/templates/nextjs/application/src/themes/metadata.base.dark.json +8 -0
  51. package/src/templates/nextjs/application/src/themes/metadata.base.json +7 -0
  52. package/src/templates/nextjs/application/src/types.tsx +60 -0
  53. package/src/templates/nextjs/application/src/utils/default-user.tsx +7 -0
  54. package/src/templates/nextjs/application/src/utils/media-query.tsx +56 -0
  55. package/src/templates/nextjs/application/src/variables.scss +53 -0
  56. package/src/templates/nextjs/page/page.scss +0 -0
  57. package/src/templates/nextjs/page/page.tsx +13 -0
  58. package/src/templates/nextjs/sample-pages/home/home.scss +37 -0
  59. package/src/templates/nextjs/sample-pages/home/page.tsx +101 -0
  60. package/src/templates/nextjs/sample-pages/profile/page.tsx +61 -0
  61. package/src/templates/nextjs/sample-pages/profile/profile.scss +19 -0
  62. package/src/templates/nextjs/sample-pages/tasks/page.tsx +112 -0
  63. package/src/templates/nextjs/sample-pages/tasks/tasks.scss +3 -0
  64. package/src/templates/react/application/src/components/header/Header.scss +1 -1
  65. package/src/templates/react/application/src/dx-styles.scss +3 -3
  66. package/src/templates/vue-v3/application/src/components/header-toolbar.vue +1 -1
  67. package/src/templates/vue-v3/application/src/dx-styles.scss +4 -3
  68. package/src/utility/latest-versions.js +6 -4
  69. package/src/utility/module.js +11 -3
  70. package/src/utility/prompts/react-app-type.js +17 -0
  71. package/src/utility/run-command.js +10 -2
  72. 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,5 @@
1
+ const appInfo = {
2
+ title: '<%=project%>'
3
+ };
4
+ export default appInfo;
5
+
@@ -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%>];
@@ -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' };
@@ -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
+ }
@@ -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,12 @@
1
+ .footer {
2
+ display: block;
3
+ color: var(--base-text-color-alpha-7);
4
+ border-top: 1px solid var(--footer-border-color);
5
+ padding-top: 20px;
6
+ padding-bottom: 24px;
7
+ margin: 0 40px;
8
+
9
+ @media (max-width: 599.99px) {
10
+ margin: 0 20px;
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import './Footer.scss';
2
+
3
+ export default function Footer({ ...rest }) {
4
+ return <footer className={'footer'} {...rest} />;
5
+ }
@@ -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,12 @@
1
+ .login-form {
2
+ .link {
3
+ text-align: center;
4
+ font-size: 12px;
5
+ font-style: normal;
6
+ margin: 6px 0 50px;
7
+ }
8
+
9
+ .form-text {
10
+ color: var(--base-text-color-alpha-7);
11
+ }
12
+ }
@@ -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' } };
@@ -0,0 +1,12 @@
1
+ .reset-password-form {
2
+ .submit-button {
3
+ margin-top: 18px;
4
+ }
5
+
6
+ .login-link {
7
+ color: var(--base-accent);
8
+ font-size: 12px;
9
+ text-align: center;
10
+ margin-top: 6px;
11
+ }
12
+ }