some wild stuff, modals, dropdowns, forms etc
This commit is contained in:
parent
fd3c4cc3af
commit
3feff2a367
@ -6,6 +6,11 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
|
.titleWrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.pictureWrapper {
|
.pictureWrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -13,6 +18,7 @@
|
|||||||
.clientInfo {
|
.clientInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { OAuth2ClientListItem } from '../../lib/types/oauth2-client.interface';
|
import {
|
||||||
|
OAuth2ClientListItem,
|
||||||
|
OAuth2ClientURL,
|
||||||
|
OAuth2ClientURLType,
|
||||||
|
} from '../../lib/types/oauth2-client.interface';
|
||||||
import { PaginatedResponse } from '../../lib/types/paginated-response.interface';
|
import { PaginatedResponse } from '../../lib/types/paginated-response.interface';
|
||||||
import { Paginator } from '../common/Paginator/Paginator';
|
import { Paginator } from '../common/Paginator/Paginator';
|
||||||
import styles from './OAuth2Page.module.scss';
|
import styles from './OAuth2Page.module.scss';
|
||||||
import { UPLOADS_URL } from '../../lib/constants';
|
import { UPLOADS_URL } from '../../lib/constants';
|
||||||
import application from '../../public/application.png';
|
import application from '../../public/application.png';
|
||||||
import { useState } from 'react';
|
import { ChangeEvent, useMemo, useRef, useState } from 'react';
|
||||||
import useUser from '../../lib/hooks/useUser';
|
import useUser from '../../lib/hooks/useUser';
|
||||||
import { Container } from '../common/Container/Container';
|
import { Container } from '../common/Container/Container';
|
||||||
import { Header } from '../common/Header/Header';
|
import { Header } from '../common/Header/Header';
|
||||||
@ -15,18 +19,147 @@ import ModalHeader from '../common/Modal/ModalHeader/ModalHeader';
|
|||||||
import ModalBody from '../common/Modal/ModalBody/ModalBody';
|
import ModalBody from '../common/Modal/ModalBody/ModalBody';
|
||||||
import ModalFooter from '../common/Modal/ModalFooter/ModalFooter';
|
import ModalFooter from '../common/Modal/ModalFooter/ModalFooter';
|
||||||
import ModalService from '../common/Modal/services/ModalService';
|
import ModalService from '../common/Modal/services/ModalService';
|
||||||
|
import { Dropdown } from '../common/Dropdown/Dropdown';
|
||||||
|
import { ModalProps } from '../../lib/types/modal.interface';
|
||||||
|
import { useForm } from '../../lib/hooks/useForm';
|
||||||
|
import { FormWrapper } from '../common/Form/FormWrapper/FormWrapper';
|
||||||
|
import { FormControl } from '../common/Form/FormControl/FormControl';
|
||||||
|
|
||||||
const TestModal = ({ close }: { close: (...args: any[]) => void }) => {
|
const LINK_NAMES = {
|
||||||
|
redirect_uri: 'Redirect URI',
|
||||||
|
terms: 'Terms of Service',
|
||||||
|
privacy: 'Privacy Policy',
|
||||||
|
website: 'Website',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinkEdit = ({
|
||||||
|
formData,
|
||||||
|
handleInputChange,
|
||||||
|
linkType,
|
||||||
|
}: {
|
||||||
|
formData: Partial<OAuth2ClientListItem>;
|
||||||
|
handleInputChange: (
|
||||||
|
e: ChangeEvent,
|
||||||
|
setValue?: any,
|
||||||
|
formField?: string
|
||||||
|
) => void;
|
||||||
|
linkType: OAuth2ClientURLType;
|
||||||
|
}) => {
|
||||||
|
const formUrl = useMemo<Partial<OAuth2ClientURL>>(
|
||||||
|
() => (formData.urls || []).find(({ type }) => type === linkType) || {},
|
||||||
|
[formData, linkType]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Modal>
|
<FormControl>
|
||||||
|
<label htmlFor={linkType}>{LINK_NAMES[linkType]}</label>
|
||||||
|
<input
|
||||||
|
id={linkType}
|
||||||
|
name={linkType}
|
||||||
|
value={formUrl?.url || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!formUrl.type) {
|
||||||
|
formUrl.type = linkType;
|
||||||
|
(formData.urls as Partial<OAuth2ClientURL>[])?.push(formUrl);
|
||||||
|
}
|
||||||
|
formUrl.url = e.target.value;
|
||||||
|
handleInputChange(e, formData.urls, 'urls');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditClientModal = ({
|
||||||
|
close,
|
||||||
|
modalRef,
|
||||||
|
client,
|
||||||
|
}: ModalProps<{ client?: OAuth2ClientListItem }>) => {
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
const { formData, handleInputChange, handleSubmit } = useForm<
|
||||||
|
Partial<OAuth2ClientListItem>
|
||||||
|
>(client || {}, () => {
|
||||||
|
console.log(formData);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal modalRef={modalRef}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<h3>Test!</h3>
|
<h3>Edit OAuth2 Client</h3>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p>This is only a test</p>
|
<FormWrapper>
|
||||||
|
<form ref={formRef} onSubmit={handleSubmit}>
|
||||||
|
<FormControl>
|
||||||
|
<label htmlFor="title">Title</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<label htmlFor="description">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<label htmlFor="scope">Allowed scopes</label>
|
||||||
|
<input
|
||||||
|
id="scope"
|
||||||
|
name="scope"
|
||||||
|
value={formData.scope}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<label htmlFor="grants">Allowed grant types</label>
|
||||||
|
<input
|
||||||
|
id="grants"
|
||||||
|
name="grants"
|
||||||
|
value={formData.grants}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl inline={true}>
|
||||||
|
<label htmlFor="activated">Activated</label>
|
||||||
|
<input
|
||||||
|
id="activated"
|
||||||
|
name="activated"
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.activated}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl inline={true}>
|
||||||
|
<label htmlFor="verified">Verified</label>
|
||||||
|
<input
|
||||||
|
id="verified"
|
||||||
|
name="verified"
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.verified}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<LinkEdit
|
||||||
|
formData={formData}
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
linkType={OAuth2ClientURLType.REDIRECT_URI}
|
||||||
|
></LinkEdit>
|
||||||
|
</form>
|
||||||
|
</FormWrapper>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<button onClick={() => close(true)}>Close</button>
|
<button onClick={() => close(true)}>Cancel</button>
|
||||||
|
<button>New secret</button>
|
||||||
|
<button onClick={() => handleSubmit()}>Submit</button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@ -47,7 +180,19 @@ const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.clientInfo}>
|
<div className={styles.clientInfo}>
|
||||||
|
<div className={styles.titleWrap}>
|
||||||
<h2>{client.title}</h2>
|
<h2>{client.title}</h2>
|
||||||
|
<Dropdown opens="right">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
ModalService.open(EditClientModal, { client: client })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit client
|
||||||
|
</button>
|
||||||
|
{!client.activated && <button>Delete client</button>}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
<span className={styles.clientDescription}>{client.description}</span>
|
<span className={styles.clientDescription}>{client.description}</span>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Client ID</dt>
|
<dt>Client ID</dt>
|
||||||
@ -84,7 +229,6 @@ const OAuth2ClientList = ({
|
|||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<>
|
<>
|
||||||
<button onClick={() => ModalService.open(TestModal)}>test</button>
|
|
||||||
<div className={styles.clientList}>
|
<div className={styles.clientList}>
|
||||||
{data.list.map((client) => (
|
{data.list.map((client) => (
|
||||||
<OAuth2ClientCard client={client} key={client.client_id} />
|
<OAuth2ClientCard client={client} key={client.client_id} />
|
||||||
|
77
components/common/Dropdown/Dropdown.module.scss
Normal file
77
components/common/Dropdown/Dropdown.module.scss
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background-color: rgb(240, 240, 240);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-bottom: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '⁝';
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
top: 100%;
|
||||||
|
min-width: 140px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
a,
|
||||||
|
button {
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem 0.5rem;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background-color: rgb(240, 240, 240);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
.dropdown {
|
||||||
|
right: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
.dropdown {
|
||||||
|
left: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
components/common/Dropdown/Dropdown.tsx
Normal file
45
components/common/Dropdown/Dropdown.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import conditionalClass from '../../../lib/utils/conditional-class';
|
||||||
|
import styles from './Dropdown.module.scss';
|
||||||
|
|
||||||
|
export const Dropdown = ({
|
||||||
|
children,
|
||||||
|
opens = 'left',
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
opens?: 'right' | 'left';
|
||||||
|
}) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Off click listener
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible || !ref) return;
|
||||||
|
|
||||||
|
function handleClickOutside(event: MouseEvent | TouchEvent) {
|
||||||
|
if (ref.current && !ref.current.contains(event.target as HTMLElement)) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('touchstart', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('touchstart', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [ref, visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[styles.wrapper, styles[opens]].join(' ')} ref={ref}>
|
||||||
|
<button
|
||||||
|
className={[
|
||||||
|
styles.toggle,
|
||||||
|
conditionalClass(visible, styles.active),
|
||||||
|
].join(' ')}
|
||||||
|
onClick={() => setVisible(!visible)}
|
||||||
|
></button>
|
||||||
|
{visible && <div className={styles.dropdown}>{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
20
components/common/Form/Form.module.scss
Normal file
20
components/common/Form/Form.module.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.wrapper {
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
components/common/Form/FormControl/FormControl.tsx
Normal file
20
components/common/Form/FormControl/FormControl.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import conditionalClass from '../../../../lib/utils/conditional-class';
|
||||||
|
import styles from '../Form.module.scss';
|
||||||
|
|
||||||
|
export const FormControl = ({
|
||||||
|
children,
|
||||||
|
inline = false,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
inline?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[styles.control, conditionalClass(inline, styles.inline)].join(
|
||||||
|
' '
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
5
components/common/Form/FormWrapper/FormWrapper.tsx
Normal file
5
components/common/Form/FormWrapper/FormWrapper.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import styles from '../Form.module.scss';
|
||||||
|
|
||||||
|
export const FormWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <div className={styles.wrapper}>{children}</div>;
|
||||||
|
};
|
@ -21,11 +21,14 @@
|
|||||||
|
|
||||||
.body {
|
.body {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
|
gap: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
|
import { RefObject } from 'react';
|
||||||
import styles from './Modal.module.scss';
|
import styles from './Modal.module.scss';
|
||||||
|
|
||||||
export default function Modal({ children }: { children: JSX.Element[] }) {
|
export default function Modal({
|
||||||
return <div className={styles.modal}>{children}</div>;
|
children,
|
||||||
|
modalRef,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
modalRef: RefObject<HTMLDivElement>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={styles.modal} ref={modalRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import styles from '../Modal/Modal.module.scss';
|
import styles from '../Modal/Modal.module.scss';
|
||||||
|
|
||||||
export default function ModalBody({
|
export default function ModalBody({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: JSX.Element | [];
|
|
||||||
}) {
|
|
||||||
return <div className={styles.body}>{children}</div>;
|
return <div className={styles.body}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import styles from '../Modal/Modal.module.scss';
|
|||||||
export default function ModalFooter({
|
export default function ModalFooter({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: JSX.Element | [];
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return <div className={styles.footer}>{children}</div>;
|
return <div className={styles.footer}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import styles from '../Modal/Modal.module.scss';
|
|||||||
export default function ModalHeader({
|
export default function ModalHeader({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: JSX.Element | [];
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return <div className={styles.header}>{children}</div>;
|
return <div className={styles.header}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { ModalDetail } from '../../../../lib/types/modal.interface';
|
import { ModalSetter } from '../../../../lib/types/modal.interface';
|
||||||
import ModalService from '../services/ModalService';
|
import ModalService from '../services/ModalService';
|
||||||
import styles from './ModalRoot.module.scss';
|
import styles from './ModalRoot.module.scss';
|
||||||
|
|
||||||
export default function ModalRoot() {
|
export default function ModalRoot() {
|
||||||
const [modal, setModal] = useState<ModalDetail<unknown> | null>(null);
|
const [modal, setModal] = useState<ModalSetter<Object> | null>(null);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ModalService.on('open', ({ component, props, target, resolve }) => {
|
ModalService.on('open', ({ component, props, target, resolve }) => {
|
||||||
setModal({
|
setModal({
|
||||||
component,
|
component,
|
||||||
props,
|
props,
|
||||||
|
resolve,
|
||||||
close: (...args) => {
|
close: (...args) => {
|
||||||
setModal(null);
|
setModal(null);
|
||||||
resolve?.call(null, ...args);
|
resolve?.call(null, ...args);
|
||||||
@ -21,19 +23,36 @@ export default function ModalRoot() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
modal?.close?.call(null, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keyup', handler);
|
function handleEscapeKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
modal?.close?.call(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Off click listener
|
||||||
|
function handleClickOutside(event: MouseEvent | TouchEvent) {
|
||||||
|
if (
|
||||||
|
ref.current &&
|
||||||
|
!(ref.current as unknown as HTMLElement).contains(
|
||||||
|
event.target as HTMLElement
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
modal?.close?.call(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keyup', handleEscapeKey);
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('touchstart', handleClickOutside);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keyup', handler);
|
window.removeEventListener('keyup', handleEscapeKey);
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('touchstart', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [modal]);
|
}, [modal]);
|
||||||
|
|
||||||
@ -42,11 +61,7 @@ export default function ModalRoot() {
|
|||||||
return (
|
return (
|
||||||
<section className={modal?.component ? styles.modalRoot : ''}>
|
<section className={modal?.component ? styles.modalRoot : ''}>
|
||||||
{ModalComponent && (
|
{ModalComponent && (
|
||||||
<ModalComponent
|
<ModalComponent {...modal?.props} modalRef={ref} close={modal!.close} />
|
||||||
{...modal?.props}
|
|
||||||
close={modal?.close}
|
|
||||||
className={ModalComponent ? 'd-block' : ''}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { ModalType, ModalDetail } from '../../../../lib/types/modal.interface';
|
import {
|
||||||
|
ModalType,
|
||||||
|
ModalEventDetail,
|
||||||
|
} from '../../../../lib/types/modal.interface';
|
||||||
|
|
||||||
const ModalService = {
|
const ModalService = {
|
||||||
on(event: string, callback: (props: ModalDetail<unknown>) => void) {
|
on(event: string, callback: (props: ModalEventDetail<Object>) => void) {
|
||||||
document.addEventListener(event, (e: Event) =>
|
document.addEventListener(event, (e: Event) =>
|
||||||
callback((e as CustomEvent<ModalDetail<unknown>>).detail)
|
callback((e as CustomEvent<ModalEventDetail<Object>>).detail)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
open<T>(component: ModalType<T>, props: any = {}) {
|
open<T>(component: ModalType<T>, props: any = {}) {
|
||||||
return new Promise((resolve, _) => {
|
return new Promise((resolve, _) => {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new CustomEvent<ModalDetail<T>>('open', {
|
new CustomEvent<ModalEventDetail<T>>('open', {
|
||||||
detail: {
|
detail: {
|
||||||
component,
|
component,
|
||||||
props,
|
props,
|
||||||
|
24
lib/hooks/useForm.ts
Normal file
24
lib/hooks/useForm.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ChangeEvent, FormEvent, useState } from 'react';
|
||||||
|
|
||||||
|
export function useForm<T>(initialState: T, onSubmit: (data: T) => void) {
|
||||||
|
const [formData, setFormData] = useState<T>(initialState);
|
||||||
|
|
||||||
|
const handleInputChange = (
|
||||||
|
e: ChangeEvent,
|
||||||
|
setValue?: any,
|
||||||
|
formField?: string
|
||||||
|
) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[formField || target.name]: setValue ?? target.checked ?? target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e?: FormEvent) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
onSubmit(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { formData, handleInputChange, handleSubmit };
|
||||||
|
}
|
@ -1,13 +1,19 @@
|
|||||||
import React from 'react';
|
import React, { RefObject } from 'react';
|
||||||
|
|
||||||
export type ModalType<T> = React.ElementType<
|
export type ModalProps<T = Object> = T & {
|
||||||
T & { close: (...args: any[]) => void }
|
close: (...args: any[]) => void;
|
||||||
>;
|
modalRef: RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ModalDetail<T> {
|
export type ModalType<T> = React.ElementType<any & ModalProps<T>>;
|
||||||
component: ModalType<T>;
|
|
||||||
props: any;
|
export interface ModalEventDetail<K = Object> {
|
||||||
|
component: ModalType<K>;
|
||||||
|
props: K;
|
||||||
|
resolve: (...args: any[]) => void;
|
||||||
target?: HTMLElement;
|
target?: HTMLElement;
|
||||||
resolve?: (...args: any[]) => void;
|
}
|
||||||
close?: (...args: any[]) => void;
|
|
||||||
|
export interface ModalSetter<K = Object> extends ModalEventDetail<K> {
|
||||||
|
close: (...args: any[]) => void;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user