kaze 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +42 -0
  4. data/bin/kaze +11 -0
  5. data/lib/kaze/commands/install_command.rb +63 -0
  6. data/lib/kaze/commands/install_inertia_stacks.rb +151 -0
  7. data/lib/kaze/commands.rb +2 -0
  8. data/lib/kaze/version.rb +3 -0
  9. data/lib/kaze.rb +9 -0
  10. data/stubs/default/Procfile.dev +3 -0
  11. data/stubs/default/app/assets/stylesheets/application.css +1 -0
  12. data/stubs/default/app/assets/stylesheets/application.tailwind.css +3 -0
  13. data/stubs/default/app/controllers/application_controller.rb +5 -0
  14. data/stubs/default/app/controllers/auth/authenticated_session_controller.rb +28 -0
  15. data/stubs/default/app/controllers/auth/new_password_controller.rb +18 -0
  16. data/stubs/default/app/controllers/auth/password_reset_link_controller.rb +17 -0
  17. data/stubs/default/app/controllers/auth/registered_user_controller.rb +19 -0
  18. data/stubs/default/app/controllers/concerns/authenticate.rb +34 -0
  19. data/stubs/default/app/controllers/concerns/handle_inertia_requests.rb +9 -0
  20. data/stubs/default/app/controllers/concerns/verify_csrf_token.rb +24 -0
  21. data/stubs/default/app/controllers/dashboard_controller.rb +5 -0
  22. data/stubs/default/app/controllers/password_controller.rb +11 -0
  23. data/stubs/default/app/controllers/profile_controller.rb +31 -0
  24. data/stubs/default/app/controllers/welcome_controller.rb +10 -0
  25. data/stubs/default/app/forms/application_form.rb +9 -0
  26. data/stubs/default/app/forms/auth/login_form.rb +18 -0
  27. data/stubs/default/app/forms/auth/new_password_form.rb +21 -0
  28. data/stubs/default/app/forms/auth/register_form.rb +7 -0
  29. data/stubs/default/app/forms/auth/send_password_reset_link_form.rb +22 -0
  30. data/stubs/default/app/forms/delete_user_form.rb +5 -0
  31. data/stubs/default/app/forms/update_password_form.rb +6 -0
  32. data/stubs/default/app/forms/update_profile_information_form.rb +6 -0
  33. data/stubs/default/app/mailers/application_mailer.rb +11 -0
  34. data/stubs/default/app/mailers/user_mailer.rb +8 -0
  35. data/stubs/default/app/models/application_record.rb +3 -0
  36. data/stubs/default/app/models/concerns/can_reset_password.rb +5 -0
  37. data/stubs/default/app/models/current.rb +3 -0
  38. data/stubs/default/app/models/user.rb +11 -0
  39. data/stubs/default/app/validators/current_password_validator.rb +5 -0
  40. data/stubs/default/app/validators/email_validator.rb +7 -0
  41. data/stubs/default/app/validators/lowercase_validator.rb +5 -0
  42. data/stubs/default/app/validators/uniqueness_validator.rb +24 -0
  43. data/stubs/default/app/views/layouts/mailer.html.erb +374 -0
  44. data/stubs/default/app/views/layouts/mailer.text.erb +11 -0
  45. data/stubs/default/app/views/user_mailer/reset_password.html.erb +39 -0
  46. data/stubs/default/bin/dev +16 -0
  47. data/stubs/default/bin/vite +27 -0
  48. data/stubs/default/config/routes.rb +27 -0
  49. data/stubs/default/config/vite.json +16 -0
  50. data/stubs/default/db/migrate/20240101000000_create_users.rb +14 -0
  51. data/stubs/default/db/migrate/20240101000001_create_delayed_jobs.rb +22 -0
  52. data/stubs/inertia-react-ts/app/javascript/Components/ApplicationLogo.tsx +12 -0
  53. data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +14 -0
  54. data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +17 -0
  55. data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +99 -0
  56. data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +9 -0
  57. data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +9 -0
  58. data/stubs/inertia-react-ts/app/javascript/Components/Modal.tsx +68 -0
  59. data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +18 -0
  60. data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +17 -0
  61. data/stubs/inertia-react-ts/app/javascript/Components/ResponsiveNavLink.tsx +16 -0
  62. data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +18 -0
  63. data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +30 -0
  64. data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +131 -0
  65. data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +19 -0
  66. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +52 -0
  67. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +98 -0
  68. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +118 -0
  69. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +74 -0
  70. data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +22 -0
  71. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +33 -0
  72. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +100 -0
  73. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +114 -0
  74. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +84 -0
  75. data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +66 -0
  76. data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +34 -0
  77. data/stubs/inertia-react-ts/app/javascript/entrypoints/bootstrap.ts +4 -0
  78. data/stubs/inertia-react-ts/app/javascript/types/global.d.ts +7 -0
  79. data/stubs/inertia-react-ts/app/javascript/types/index.d.ts +12 -0
  80. data/stubs/inertia-react-ts/app/javascript/types/vite-env.d.ts +1 -0
  81. data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +26 -0
  82. data/stubs/inertia-react-ts/config/tailwind.config.js +22 -0
  83. data/stubs/inertia-react-ts/package.json +26 -0
  84. data/stubs/inertia-react-ts/tsconfig.json +19 -0
  85. data/stubs/inertia-react-ts/vite.config.ts +13 -0
  86. data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +8 -0
  87. data/stubs/inertia-vue-ts/app/javascript/Components/Checkbox.vue +29 -0
  88. data/stubs/inertia-vue-ts/app/javascript/Components/DangerButton.vue +7 -0
  89. data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +75 -0
  90. data/stubs/inertia-vue-ts/app/javascript/Components/DropdownLink.vue +16 -0
  91. data/stubs/inertia-vue-ts/app/javascript/Components/InputError.vue +13 -0
  92. data/stubs/inertia-vue-ts/app/javascript/Components/InputLabel.vue +12 -0
  93. data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +96 -0
  94. data/stubs/inertia-vue-ts/app/javascript/Components/NavLink.vue +21 -0
  95. data/stubs/inertia-vue-ts/app/javascript/Components/PrimaryButton.vue +7 -0
  96. data/stubs/inertia-vue-ts/app/javascript/Components/ResponsiveNavLink.vue +21 -0
  97. data/stubs/inertia-vue-ts/app/javascript/Components/SecondaryButton.vue +19 -0
  98. data/stubs/inertia-vue-ts/app/javascript/Components/TextInput.vue +23 -0
  99. data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +155 -0
  100. data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +20 -0
  101. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +60 -0
  102. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +93 -0
  103. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +106 -0
  104. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +89 -0
  105. data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +22 -0
  106. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +42 -0
  107. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +98 -0
  108. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +108 -0
  109. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +78 -0
  110. data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +56 -0
  111. data/stubs/inertia-vue-ts/app/javascript/entrypoints/application.ts +34 -0
  112. data/stubs/inertia-vue-ts/app/javascript/entrypoints/bootstrap.ts +4 -0
  113. data/stubs/inertia-vue-ts/app/javascript/types/global.d.ts +13 -0
  114. data/stubs/inertia-vue-ts/app/javascript/types/index.d.ts +12 -0
  115. data/stubs/inertia-vue-ts/app/javascript/types/vite-env.d.ts +1 -0
  116. data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +25 -0
  117. data/stubs/inertia-vue-ts/config/tailwind.config.js +22 -0
  118. data/stubs/inertia-vue-ts/package.json +24 -0
  119. data/stubs/inertia-vue-ts/tsconfig.json +19 -0
  120. data/stubs/inertia-vue-ts/vite.config.ts +13 -0
  121. metadata +205 -0
@@ -0,0 +1,14 @@
1
+ import { InputHTMLAttributes } from 'react';
2
+
3
+ export default function Checkbox({ className = '', ...props }: InputHTMLAttributes<HTMLInputElement>) {
4
+ return (
5
+ <input
6
+ {...props}
7
+ type="checkbox"
8
+ className={
9
+ 'rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800 ' +
10
+ className
11
+ }
12
+ />
13
+ );
14
+ }
@@ -0,0 +1,17 @@
1
+ import { ButtonHTMLAttributes } from 'react';
2
+
3
+ export default function DangerButton({ className = '', disabled, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
4
+ return (
5
+ <button
6
+ {...props}
7
+ className={
8
+ `inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 ${
9
+ disabled && 'opacity-25'
10
+ } ` + className
11
+ }
12
+ disabled={disabled}
13
+ >
14
+ {children}
15
+ </button>
16
+ );
17
+ }
@@ -0,0 +1,99 @@
1
+ import { useState, createContext, useContext, Fragment, PropsWithChildren, Dispatch, SetStateAction } from 'react';
2
+ import { Link, InertiaLinkProps } from '@inertiajs/react';
3
+ import { Transition } from '@headlessui/react';
4
+
5
+ const DropDownContext = createContext<{
6
+ open: boolean;
7
+ setOpen: Dispatch<SetStateAction<boolean>>;
8
+ toggleOpen: () => void;
9
+ }>({
10
+ open: false,
11
+ setOpen: () => {},
12
+ toggleOpen: () => {},
13
+ });
14
+
15
+ const Dropdown = ({ children }: PropsWithChildren) => {
16
+ const [open, setOpen] = useState(false);
17
+
18
+ const toggleOpen = () => {
19
+ setOpen((previousState) => !previousState);
20
+ };
21
+
22
+ return (
23
+ <DropDownContext.Provider value={{ open, setOpen, toggleOpen }}>
24
+ <div className="relative">{children}</div>
25
+ </DropDownContext.Provider>
26
+ );
27
+ };
28
+
29
+ const Trigger = ({ children }: PropsWithChildren) => {
30
+ const { open, setOpen, toggleOpen } = useContext(DropDownContext);
31
+
32
+ return (
33
+ <>
34
+ <div onClick={toggleOpen}>{children}</div>
35
+
36
+ {open && <div className="fixed inset-0 z-40" onClick={() => setOpen(false)}></div>}
37
+ </>
38
+ );
39
+ };
40
+
41
+ const Content = ({ align = 'right', width = '48', contentClasses = 'py-1 bg-white dark:bg-gray-700', children }: PropsWithChildren<{ align?: 'left'|'right', width?: '48', contentClasses?: string }>) => {
42
+ const { open, setOpen } = useContext(DropDownContext);
43
+
44
+ let alignmentClasses = 'origin-top';
45
+
46
+ if (align === 'left') {
47
+ alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0';
48
+ } else if (align === 'right') {
49
+ alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0';
50
+ }
51
+
52
+ let widthClasses = '';
53
+
54
+ if (width === '48') {
55
+ widthClasses = 'w-48';
56
+ }
57
+
58
+ return (
59
+ <>
60
+ <Transition
61
+ as={Fragment}
62
+ show={open}
63
+ enter="transition ease-out duration-200"
64
+ enterFrom="opacity-0 scale-95"
65
+ enterTo="opacity-100 scale-100"
66
+ leave="transition ease-in duration-75"
67
+ leaveFrom="opacity-100 scale-100"
68
+ leaveTo="opacity-0 scale-95"
69
+ >
70
+ <div
71
+ className={`absolute z-50 mt-2 rounded-md shadow-lg ${alignmentClasses} ${widthClasses}`}
72
+ onClick={() => setOpen(false)}
73
+ >
74
+ <div className={`rounded-md ring-1 ring-black ring-opacity-5 ` + contentClasses}>{children}</div>
75
+ </div>
76
+ </Transition>
77
+ </>
78
+ );
79
+ };
80
+
81
+ const DropdownLink = ({ className = '', children, ...props }: InertiaLinkProps) => {
82
+ return (
83
+ <Link
84
+ {...props}
85
+ className={
86
+ 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out ' +
87
+ className
88
+ }
89
+ >
90
+ {children}
91
+ </Link>
92
+ );
93
+ };
94
+
95
+ Dropdown.Trigger = Trigger;
96
+ Dropdown.Content = Content;
97
+ Dropdown.Link = DropdownLink;
98
+
99
+ export default Dropdown;
@@ -0,0 +1,9 @@
1
+ import { HTMLAttributes } from 'react';
2
+
3
+ export default function InputError({ message, className = '', ...props }: HTMLAttributes<HTMLParagraphElement> & { message?: string }) {
4
+ return message ? (
5
+ <p {...props} className={'text-sm text-red-600 dark:text-red-400 ' + className}>
6
+ {message}
7
+ </p>
8
+ ) : null;
9
+ }
@@ -0,0 +1,9 @@
1
+ import { LabelHTMLAttributes } from 'react';
2
+
3
+ export default function InputLabel({ value, className = '', children, ...props }: LabelHTMLAttributes<HTMLLabelElement> & { value?: string }) {
4
+ return (
5
+ <label {...props} className={`block font-medium text-sm text-gray-700 dark:text-gray-300 ` + className}>
6
+ {value ? value : children}
7
+ </label>
8
+ );
9
+ }
@@ -0,0 +1,68 @@
1
+ import { Fragment, PropsWithChildren } from 'react';
2
+ import { Dialog, Transition } from '@headlessui/react';
3
+
4
+ export default function Modal({
5
+ children,
6
+ show = false,
7
+ maxWidth = '2xl',
8
+ closeable = true,
9
+ onClose = () => {},
10
+ }: PropsWithChildren<{
11
+ show: boolean;
12
+ maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
13
+ closeable?: boolean;
14
+ onClose: CallableFunction;
15
+ }>) {
16
+ const close = () => {
17
+ if (closeable) {
18
+ onClose();
19
+ }
20
+ };
21
+
22
+ const maxWidthClass = {
23
+ sm: 'sm:max-w-sm',
24
+ md: 'sm:max-w-md',
25
+ lg: 'sm:max-w-lg',
26
+ xl: 'sm:max-w-xl',
27
+ '2xl': 'sm:max-w-2xl',
28
+ }[maxWidth];
29
+
30
+ return (
31
+ <Transition show={show} as={Fragment} leave="duration-200">
32
+ <Dialog
33
+ as="div"
34
+ id="modal"
35
+ className="fixed inset-0 flex overflow-y-auto px-4 py-6 sm:px-0 items-center z-50 transform transition-all"
36
+ onClose={close}
37
+ >
38
+ <Transition.Child
39
+ as={Fragment}
40
+ enter="ease-out duration-300"
41
+ enterFrom="opacity-0"
42
+ enterTo="opacity-100"
43
+ leave="ease-in duration-200"
44
+ leaveFrom="opacity-100"
45
+ leaveTo="opacity-0"
46
+ >
47
+ <div className="absolute inset-0 bg-gray-500/75 dark:bg-gray-900/75" />
48
+ </Transition.Child>
49
+
50
+ <Transition.Child
51
+ as={Fragment}
52
+ enter="ease-out duration-300"
53
+ enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
54
+ enterTo="opacity-100 translate-y-0 sm:scale-100"
55
+ leave="ease-in duration-200"
56
+ leaveFrom="opacity-100 translate-y-0 sm:scale-100"
57
+ leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
58
+ >
59
+ <Dialog.Panel
60
+ className={`mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto ${maxWidthClass}`}
61
+ >
62
+ {children}
63
+ </Dialog.Panel>
64
+ </Transition.Child>
65
+ </Dialog>
66
+ </Transition>
67
+ );
68
+ }
@@ -0,0 +1,18 @@
1
+ import { Link, InertiaLinkProps } from '@inertiajs/react';
2
+
3
+ export default function NavLink({ active = false, className = '', children, ...props }: InertiaLinkProps & { active: boolean }) {
4
+ return (
5
+ <Link
6
+ {...props}
7
+ className={
8
+ 'inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out focus:outline-none ' +
9
+ (active
10
+ ? 'border-indigo-400 dark:border-indigo-600 text-gray-900 dark:text-gray-100 focus:border-indigo-700 '
11
+ : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 ') +
12
+ className
13
+ }
14
+ >
15
+ {children}
16
+ </Link>
17
+ );
18
+ }
@@ -0,0 +1,17 @@
1
+ import { ButtonHTMLAttributes } from 'react';
2
+
3
+ export default function PrimaryButton({ className = '', disabled, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
4
+ return (
5
+ <button
6
+ {...props}
7
+ className={
8
+ `inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 ${
9
+ disabled && 'opacity-25'
10
+ } ` + className
11
+ }
12
+ disabled={disabled}
13
+ >
14
+ {children}
15
+ </button>
16
+ );
17
+ }
@@ -0,0 +1,16 @@
1
+ import { Link, InertiaLinkProps } from '@inertiajs/react';
2
+
3
+ export default function ResponsiveNavLink({ active = false, className = '', children, ...props }: InertiaLinkProps & { active?: boolean }) {
4
+ return (
5
+ <Link
6
+ {...props}
7
+ className={`w-full flex items-start ps-3 pe-4 py-2 border-l-4 ${
8
+ active
9
+ ? 'border-indigo-400 dark:border-indigo-600 text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300'
10
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600'
11
+ } text-base font-medium focus:outline-none transition duration-150 ease-in-out ${className}`}
12
+ >
13
+ {children}
14
+ </Link>
15
+ );
16
+ }
@@ -0,0 +1,18 @@
1
+ import { ButtonHTMLAttributes } from 'react';
2
+
3
+ export default function SecondaryButton({ type = 'button', className = '', disabled, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
4
+ return (
5
+ <button
6
+ {...props}
7
+ type={type}
8
+ className={
9
+ `inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150 ${
10
+ disabled && 'opacity-25'
11
+ } ` + className
12
+ }
13
+ disabled={disabled}
14
+ >
15
+ {children}
16
+ </button>
17
+ );
18
+ }
@@ -0,0 +1,30 @@
1
+ import { forwardRef, useEffect, useImperativeHandle, useRef, InputHTMLAttributes } from 'react';
2
+
3
+ export default forwardRef(function TextInput(
4
+ { type = 'text', className = '', isFocused = false, ...props }: InputHTMLAttributes<HTMLInputElement> & { isFocused?: boolean },
5
+ ref
6
+ ) {
7
+ const localRef = useRef<HTMLInputElement>(null);
8
+
9
+ useImperativeHandle(ref, () => ({
10
+ focus: () => localRef.current?.focus(),
11
+ }));
12
+
13
+ useEffect(() => {
14
+ if (isFocused) {
15
+ localRef.current?.focus();
16
+ }
17
+ }, []);
18
+
19
+ return (
20
+ <input
21
+ {...props}
22
+ type={type}
23
+ className={
24
+ 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm ' +
25
+ className
26
+ }
27
+ ref={localRef}
28
+ />
29
+ );
30
+ });
@@ -0,0 +1,131 @@
1
+ import { useState, PropsWithChildren, ReactNode } from 'react';
2
+ import ApplicationLogo from '@/Components/ApplicationLogo';
3
+ import Dropdown from '@/Components/Dropdown';
4
+ import NavLink from '@/Components/NavLink';
5
+ import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
6
+ import { Link } from '@inertiajs/react';
7
+ import { User } from '@/types';
8
+ import { dashboard_path, logout_path, profile_edit_path } from '@/routes';
9
+
10
+ export default function Authenticated({ user, header, children }: PropsWithChildren<{ user: User, header?: ReactNode }>) {
11
+ const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false);
12
+
13
+ const { pathname = '' } = typeof window !== 'undefined' ? window.location : {};
14
+
15
+ return (
16
+ <div className="min-h-screen bg-gray-100">
17
+ <nav className="bg-white border-b border-gray-100">
18
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
19
+ <div className="flex justify-between h-16">
20
+ <div className="flex">
21
+ <div className="shrink-0 flex items-center">
22
+ <Link href="/">
23
+ <ApplicationLogo className="block h-9 w-auto fill-current text-gray-800" />
24
+ </Link>
25
+ </div>
26
+
27
+ <div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
28
+ <NavLink href={dashboard_path()} active={pathname.match(/dashboard/) != null}>
29
+ Dashboard
30
+ </NavLink>
31
+ </div>
32
+ </div>
33
+
34
+ <div className="hidden sm:flex sm:items-center sm:ms-6">
35
+ <div className="ms-3 relative">
36
+ <Dropdown>
37
+ <Dropdown.Trigger>
38
+ <span className="inline-flex rounded-md">
39
+ <button
40
+ type="button"
41
+ className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
42
+ >
43
+ {user.name}
44
+
45
+ <svg
46
+ className="ms-2 -me-0.5 h-4 w-4"
47
+ xmlns="http://www.w3.org/2000/svg"
48
+ viewBox="0 0 20 20"
49
+ fill="currentColor"
50
+ >
51
+ <path
52
+ fillRule="evenodd"
53
+ d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
54
+ clipRule="evenodd"
55
+ />
56
+ </svg>
57
+ </button>
58
+ </span>
59
+ </Dropdown.Trigger>
60
+
61
+ <Dropdown.Content>
62
+ <Dropdown.Link href={profile_edit_path()}>Profile</Dropdown.Link>
63
+ <Dropdown.Link href={logout_path()} method="post" as="button">
64
+ Log Out
65
+ </Dropdown.Link>
66
+ </Dropdown.Content>
67
+ </Dropdown>
68
+ </div>
69
+ </div>
70
+
71
+ <div className="-me-2 flex items-center sm:hidden">
72
+ <button
73
+ onClick={() => setShowingNavigationDropdown((previousState) => !previousState)}
74
+ className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"
75
+ >
76
+ <svg className="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
77
+ <path
78
+ className={!showingNavigationDropdown ? 'inline-flex' : 'hidden'}
79
+ strokeLinecap="round"
80
+ strokeLinejoin="round"
81
+ strokeWidth="2"
82
+ d="M4 6h16M4 12h16M4 18h16"
83
+ />
84
+ <path
85
+ className={showingNavigationDropdown ? 'inline-flex' : 'hidden'}
86
+ strokeLinecap="round"
87
+ strokeLinejoin="round"
88
+ strokeWidth="2"
89
+ d="M6 18L18 6M6 6l12 12"
90
+ />
91
+ </svg>
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <div className={(showingNavigationDropdown ? 'block' : 'hidden') + ' sm:hidden'}>
98
+ <div className="pt-2 pb-3 space-y-1">
99
+ <ResponsiveNavLink href={dashboard_path()} active={pathname.match(/dashboard/) != null}>
100
+ Dashboard
101
+ </ResponsiveNavLink>
102
+ </div>
103
+
104
+ <div className="pt-4 pb-1 border-t border-gray-200">
105
+ <div className="px-4">
106
+ <div className="font-medium text-base text-gray-800">
107
+ {user.name}
108
+ </div>
109
+ <div className="font-medium text-sm text-gray-500">{user.email}</div>
110
+ </div>
111
+
112
+ <div className="mt-3 space-y-1">
113
+ <ResponsiveNavLink href={profile_edit_path()}>Profile</ResponsiveNavLink>
114
+ <ResponsiveNavLink method="post" href={logout_path()} as="button">
115
+ Log Out
116
+ </ResponsiveNavLink>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </nav>
121
+
122
+ {header && (
123
+ <header className="bg-white shadow">
124
+ <div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">{header}</div>
125
+ </header>
126
+ )}
127
+
128
+ <main>{children}</main>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,19 @@
1
+ import ApplicationLogo from '@/Components/ApplicationLogo';
2
+ import { Link } from '@inertiajs/react';
3
+ import { PropsWithChildren } from 'react';
4
+
5
+ export default function Guest({ children }: PropsWithChildren) {
6
+ return (
7
+ <div className="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
8
+ <div>
9
+ <Link href="/">
10
+ <ApplicationLogo className="w-20 h-20 fill-current text-gray-500" />
11
+ </Link>
12
+ </div>
13
+
14
+ <div className="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
15
+ {children}
16
+ </div>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,52 @@
1
+ import GuestLayout from '@/Layouts/GuestLayout';
2
+ import InputError from '@/Components/InputError';
3
+ import PrimaryButton from '@/Components/PrimaryButton';
4
+ import TextInput from '@/Components/TextInput';
5
+ import { Head, useForm } from '@inertiajs/react';
6
+ import { FormEventHandler } from 'react';
7
+ import { password_email_path } from '@/routes';
8
+
9
+ export default function ForgotPassword({ status }: { status?: string }) {
10
+ const { data, setData, post, processing, errors } = useForm({
11
+ email: '',
12
+ });
13
+
14
+ const submit: FormEventHandler = (e) => {
15
+ e.preventDefault();
16
+
17
+ post(password_email_path());
18
+ };
19
+
20
+ return (
21
+ <GuestLayout>
22
+ <Head title="Forgot Password" />
23
+
24
+ <div className="mb-4 text-sm text-gray-600">
25
+ Forgot your password? No problem. Just let us know your email address and we will email you a password
26
+ reset link that will allow you to choose a new one.
27
+ </div>
28
+
29
+ {status && <div className="mb-4 font-medium text-sm text-green-600">{status}</div>}
30
+
31
+ <form onSubmit={submit}>
32
+ <TextInput
33
+ id="email"
34
+ type="email"
35
+ name="email"
36
+ value={data.email}
37
+ className="mt-1 block w-full"
38
+ isFocused={true}
39
+ onChange={(e) => setData('email', e.target.value)}
40
+ />
41
+
42
+ <InputError message={errors.email} className="mt-2" />
43
+
44
+ <div className="flex items-center justify-end mt-4">
45
+ <PrimaryButton className="ms-4" disabled={processing}>
46
+ Email Password Reset Link
47
+ </PrimaryButton>
48
+ </div>
49
+ </form>
50
+ </GuestLayout>
51
+ );
52
+ }
@@ -0,0 +1,98 @@
1
+ import { useEffect, FormEventHandler } from 'react';
2
+ import Checkbox from '@/Components/Checkbox';
3
+ import GuestLayout from '@/Layouts/GuestLayout';
4
+ import InputError from '@/Components/InputError';
5
+ import InputLabel from '@/Components/InputLabel';
6
+ import PrimaryButton from '@/Components/PrimaryButton';
7
+ import TextInput from '@/Components/TextInput';
8
+ import { Head, Link, useForm } from '@inertiajs/react';
9
+ import { login_path, password_request_path } from '@/routes';
10
+
11
+ export default function Login({ status, canResetPassword }: { status?: string, canResetPassword: boolean }) {
12
+ const { data, setData, post, processing, errors, reset } = useForm({
13
+ email: '',
14
+ password: '',
15
+ remember: false,
16
+ });
17
+
18
+ useEffect(() => {
19
+ return () => {
20
+ reset('password');
21
+ };
22
+ }, []);
23
+
24
+ const submit: FormEventHandler = (e) => {
25
+ e.preventDefault();
26
+
27
+ post(login_path());
28
+ };
29
+
30
+ return (
31
+ <GuestLayout>
32
+ <Head title="Log in" />
33
+
34
+ {status && <div className="mb-4 font-medium text-sm text-green-600">{status}</div>}
35
+
36
+ <form onSubmit={submit}>
37
+ <div>
38
+ <InputLabel htmlFor="email" value="Email" />
39
+
40
+ <TextInput
41
+ id="email"
42
+ type="email"
43
+ name="email"
44
+ value={data.email}
45
+ className="mt-1 block w-full"
46
+ autoComplete="username"
47
+ isFocused={true}
48
+ onChange={(e) => setData('email', e.target.value)}
49
+ />
50
+
51
+ <InputError message={errors.email} className="mt-2" />
52
+ </div>
53
+
54
+ <div className="mt-4">
55
+ <InputLabel htmlFor="password" value="Password" />
56
+
57
+ <TextInput
58
+ id="password"
59
+ type="password"
60
+ name="password"
61
+ value={data.password}
62
+ className="mt-1 block w-full"
63
+ autoComplete="current-password"
64
+ onChange={(e) => setData('password', e.target.value)}
65
+ />
66
+
67
+ <InputError message={errors.password} className="mt-2" />
68
+ </div>
69
+
70
+ <div className="block mt-4">
71
+ <label className="flex items-center">
72
+ <Checkbox
73
+ name="remember"
74
+ checked={data.remember}
75
+ onChange={(e) => setData('remember', e.target.checked)}
76
+ />
77
+ <span className="ms-2 text-sm text-gray-600">Remember me</span>
78
+ </label>
79
+ </div>
80
+
81
+ <div className="flex items-center justify-end mt-4">
82
+ {canResetPassword && (
83
+ <Link
84
+ href={password_request_path()}
85
+ className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
86
+ >
87
+ Forgot your password?
88
+ </Link>
89
+ )}
90
+
91
+ <PrimaryButton className="ms-4" disabled={processing}>
92
+ Log in
93
+ </PrimaryButton>
94
+ </div>
95
+ </form>
96
+ </GuestLayout>
97
+ );
98
+ }