kaze 0.2.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 (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
+ }