kaze 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +42 -0
- data/bin/kaze +11 -0
- data/lib/kaze/commands/install_command.rb +63 -0
- data/lib/kaze/commands/install_inertia_stacks.rb +151 -0
- data/lib/kaze/commands.rb +2 -0
- data/lib/kaze/version.rb +3 -0
- data/lib/kaze.rb +9 -0
- data/stubs/default/Procfile.dev +3 -0
- data/stubs/default/app/assets/stylesheets/application.css +1 -0
- data/stubs/default/app/assets/stylesheets/application.tailwind.css +3 -0
- data/stubs/default/app/controllers/application_controller.rb +5 -0
- data/stubs/default/app/controllers/auth/authenticated_session_controller.rb +28 -0
- data/stubs/default/app/controllers/auth/new_password_controller.rb +18 -0
- data/stubs/default/app/controllers/auth/password_reset_link_controller.rb +17 -0
- data/stubs/default/app/controllers/auth/registered_user_controller.rb +19 -0
- data/stubs/default/app/controllers/concerns/authenticate.rb +34 -0
- data/stubs/default/app/controllers/concerns/handle_inertia_requests.rb +9 -0
- data/stubs/default/app/controllers/concerns/verify_csrf_token.rb +24 -0
- data/stubs/default/app/controllers/dashboard_controller.rb +5 -0
- data/stubs/default/app/controllers/password_controller.rb +11 -0
- data/stubs/default/app/controllers/profile_controller.rb +31 -0
- data/stubs/default/app/controllers/welcome_controller.rb +10 -0
- data/stubs/default/app/forms/application_form.rb +9 -0
- data/stubs/default/app/forms/auth/login_form.rb +18 -0
- data/stubs/default/app/forms/auth/new_password_form.rb +21 -0
- data/stubs/default/app/forms/auth/register_form.rb +7 -0
- data/stubs/default/app/forms/auth/send_password_reset_link_form.rb +22 -0
- data/stubs/default/app/forms/delete_user_form.rb +5 -0
- data/stubs/default/app/forms/update_password_form.rb +6 -0
- data/stubs/default/app/forms/update_profile_information_form.rb +6 -0
- data/stubs/default/app/mailers/application_mailer.rb +11 -0
- data/stubs/default/app/mailers/user_mailer.rb +8 -0
- data/stubs/default/app/models/application_record.rb +3 -0
- data/stubs/default/app/models/concerns/can_reset_password.rb +5 -0
- data/stubs/default/app/models/current.rb +3 -0
- data/stubs/default/app/models/user.rb +11 -0
- data/stubs/default/app/validators/current_password_validator.rb +5 -0
- data/stubs/default/app/validators/email_validator.rb +7 -0
- data/stubs/default/app/validators/lowercase_validator.rb +5 -0
- data/stubs/default/app/validators/uniqueness_validator.rb +24 -0
- data/stubs/default/app/views/layouts/mailer.html.erb +374 -0
- data/stubs/default/app/views/layouts/mailer.text.erb +11 -0
- data/stubs/default/app/views/user_mailer/reset_password.html.erb +39 -0
- data/stubs/default/bin/dev +16 -0
- data/stubs/default/bin/vite +27 -0
- data/stubs/default/config/routes.rb +27 -0
- data/stubs/default/config/vite.json +16 -0
- data/stubs/default/db/migrate/20240101000000_create_users.rb +14 -0
- data/stubs/default/db/migrate/20240101000001_create_delayed_jobs.rb +22 -0
- data/stubs/inertia-react-ts/app/javascript/Components/ApplicationLogo.tsx +12 -0
- data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +14 -0
- data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +17 -0
- data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +99 -0
- data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +9 -0
- data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +9 -0
- data/stubs/inertia-react-ts/app/javascript/Components/Modal.tsx +68 -0
- data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +18 -0
- data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +17 -0
- data/stubs/inertia-react-ts/app/javascript/Components/ResponsiveNavLink.tsx +16 -0
- data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +18 -0
- data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +30 -0
- data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +131 -0
- data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +19 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +52 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +98 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +118 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +74 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +22 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +33 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +100 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +114 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +84 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +66 -0
- data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +34 -0
- data/stubs/inertia-react-ts/app/javascript/entrypoints/bootstrap.ts +4 -0
- data/stubs/inertia-react-ts/app/javascript/types/global.d.ts +7 -0
- data/stubs/inertia-react-ts/app/javascript/types/index.d.ts +12 -0
- data/stubs/inertia-react-ts/app/javascript/types/vite-env.d.ts +1 -0
- data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +26 -0
- data/stubs/inertia-react-ts/config/tailwind.config.js +22 -0
- data/stubs/inertia-react-ts/package.json +26 -0
- data/stubs/inertia-react-ts/tsconfig.json +19 -0
- data/stubs/inertia-react-ts/vite.config.ts +13 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +8 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/Checkbox.vue +29 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/DangerButton.vue +7 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +75 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/DropdownLink.vue +16 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/InputError.vue +13 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/InputLabel.vue +12 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +96 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/NavLink.vue +21 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/PrimaryButton.vue +7 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/ResponsiveNavLink.vue +21 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/SecondaryButton.vue +19 -0
- data/stubs/inertia-vue-ts/app/javascript/Components/TextInput.vue +23 -0
- data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +155 -0
- data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +20 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +60 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +93 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +106 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +89 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +22 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +42 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +98 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +108 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +78 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +56 -0
- data/stubs/inertia-vue-ts/app/javascript/entrypoints/application.ts +34 -0
- data/stubs/inertia-vue-ts/app/javascript/entrypoints/bootstrap.ts +4 -0
- data/stubs/inertia-vue-ts/app/javascript/types/global.d.ts +13 -0
- data/stubs/inertia-vue-ts/app/javascript/types/index.d.ts +12 -0
- data/stubs/inertia-vue-ts/app/javascript/types/vite-env.d.ts +1 -0
- data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +25 -0
- data/stubs/inertia-vue-ts/config/tailwind.config.js +22 -0
- data/stubs/inertia-vue-ts/package.json +24 -0
- data/stubs/inertia-vue-ts/tsconfig.json +19 -0
- data/stubs/inertia-vue-ts/vite.config.ts +13 -0
- 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
|
+
}
|