more work on admin
This commit is contained in:
parent
9b9141d5a9
commit
19f2a820eb
@ -27,6 +27,7 @@ import { FormControl } from '../common/Form/FormControl/FormControl';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Button } from '../common/Button/Button';
|
import { Button } from '../common/Button/Button';
|
||||||
import userHasPrivileges from '../../lib/utils/has-privileges';
|
import userHasPrivileges from '../../lib/utils/has-privileges';
|
||||||
|
import { publishJSON } from '../../lib/utils/swr-fetcher';
|
||||||
|
|
||||||
const LINK_NAMES = {
|
const LINK_NAMES = {
|
||||||
redirect_uri: 'Redirect URI',
|
redirect_uri: 'Redirect URI',
|
||||||
@ -93,25 +94,13 @@ const EditClientModal = ({
|
|||||||
>(client || {}, () => {
|
>(client || {}, () => {
|
||||||
toast
|
toast
|
||||||
.promise(
|
.promise(
|
||||||
fetch(
|
publishJSON(
|
||||||
client
|
client
|
||||||
? `/api/admin/oauth2/clients/${client.id}`
|
? `/api/admin/oauth2/clients/${client.id}`
|
||||||
: '/api/admin/oauth2/clients',
|
: '/api/admin/oauth2/clients',
|
||||||
{
|
client ? 'PATCH' : 'POST',
|
||||||
method: client ? 'PATCH' : 'POST',
|
{ activated: true, ...formData }
|
||||||
headers: {
|
),
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ activated: true, ...formData }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then(async (data) => {
|
|
||||||
if (data.error) {
|
|
||||||
throw data;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
loading: 'Saving client...',
|
loading: 'Saving client...',
|
||||||
success: 'Client saved!',
|
success: 'Client saved!',
|
||||||
@ -226,16 +215,10 @@ const EditClientModal = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
toast
|
toast
|
||||||
.promise(
|
.promise(
|
||||||
fetch(`/api/admin/oauth2/clients/${client!.id}/new-secret`, {
|
publishJSON(
|
||||||
method: 'POST',
|
`/api/admin/oauth2/clients/${client!.id}/new-secret`,
|
||||||
})
|
'POST'
|
||||||
.then((res) => res.json())
|
),
|
||||||
.then(async (data) => {
|
|
||||||
if (data.error) {
|
|
||||||
throw data;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
loading: 'Generating new secret...',
|
loading: 'Generating new secret...',
|
||||||
success: 'New secret generated.',
|
success: 'New secret generated.',
|
||||||
|
@ -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 @@
|
|||||||
.userInfo {
|
.userInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -29,3 +35,33 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.privEditor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.privInner {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.described {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.privControls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import styles from './UsersPage.module.scss';
|
import styles from './UsersPage.module.scss';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import useUser from '../../lib/hooks/useUser';
|
import useUser from '../../lib/hooks/useUser';
|
||||||
import { PaginatedResponse } from '../../lib/types/paginated-response.interface';
|
import { PaginatedResponse } from '../../lib/types/paginated-response.interface';
|
||||||
@ -10,8 +10,255 @@ import avatar from '../../public/avatar.png';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { UPLOADS_URL } from '../../lib/constants';
|
import { UPLOADS_URL } from '../../lib/constants';
|
||||||
import { Paginator } from '../common/Paginator/Paginator';
|
import { Paginator } from '../common/Paginator/Paginator';
|
||||||
|
import { Dropdown } from '../common/Dropdown/Dropdown';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { publishJSON } from '../../lib/utils/swr-fetcher';
|
||||||
|
import { useForm } from '../../lib/hooks/useForm';
|
||||||
|
import { ModalProps } from '../../lib/types/modal.interface';
|
||||||
|
import { Button } from '../common/Button/Button';
|
||||||
|
import { FormControl } from '../common/Form/FormControl/FormControl';
|
||||||
|
import { FormWrapper } from '../common/Form/FormWrapper/FormWrapper';
|
||||||
|
import Modal from '../common/Modal/Modal/Modal';
|
||||||
|
import ModalBody from '../common/Modal/ModalBody/ModalBody';
|
||||||
|
import ModalFooter from '../common/Modal/ModalFooter/ModalFooter';
|
||||||
|
import ModalHeader from '../common/Modal/ModalHeader/ModalHeader';
|
||||||
|
import ModalService from '../common/Modal/services/ModalService';
|
||||||
|
import { Privilege } from '../../lib/types/privilege.interface';
|
||||||
|
import useHasPrivileges from '../../lib/hooks/useHasPrivileges';
|
||||||
|
|
||||||
const UserCard = ({ user }: { user: UserListItem }) => (
|
function getSelectValues(selectElement: HTMLSelectElement) {
|
||||||
|
const result = [];
|
||||||
|
const options = selectElement && selectElement.options;
|
||||||
|
|
||||||
|
for (let i = 0, iLen = options.length; i < iLen; i++) {
|
||||||
|
const opt = options[i];
|
||||||
|
|
||||||
|
if (opt.selected) {
|
||||||
|
result.push(opt.value || opt.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrivilegeEditor = ({
|
||||||
|
user,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
user: UserListItem;
|
||||||
|
onChange: (privvy: Privilege[]) => void;
|
||||||
|
}) => {
|
||||||
|
const [toggle, setToggle] = useState(false);
|
||||||
|
const [selectionAvailable, setSelectionAvailable] = useState<string[]>([]);
|
||||||
|
const [selectionExisting, setSelectionExisting] = useState<string[]>([]);
|
||||||
|
const [availablePrivileges, setAvailablePrivileges] = useState<Privilege[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const { data } = useSWR<Privilege[]>('/api/admin/privileges');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAvailablePrivileges(
|
||||||
|
(data || []).filter(
|
||||||
|
({ id }) => !(user.privileges || []).some((privvy) => privvy.id === id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [user, data, toggle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.privEditor}>
|
||||||
|
<h3>Privileges</h3>
|
||||||
|
<div className={styles.privInner}>
|
||||||
|
<div className={styles.described}>
|
||||||
|
<h4>Available</h4>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectionAvailable}
|
||||||
|
onChange={(e) => setSelectionAvailable(getSelectValues(e.target))}
|
||||||
|
>
|
||||||
|
{availablePrivileges.map(({ name, id }) => (
|
||||||
|
<option value={id} key={id}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.privControls}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!selectionAvailable.length}
|
||||||
|
onClick={() => {
|
||||||
|
// Add privileges
|
||||||
|
const toAdd = availablePrivileges.filter(({ id }) =>
|
||||||
|
selectionAvailable.includes(id.toString())
|
||||||
|
);
|
||||||
|
user.privileges = [...(user.privileges || []), ...toAdd];
|
||||||
|
setToggle(!toggle);
|
||||||
|
setSelectionAvailable([]);
|
||||||
|
onChange(user.privileges);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
>>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!selectionExisting.length}
|
||||||
|
onClick={() => {
|
||||||
|
// Remove privileges
|
||||||
|
user.privileges = (user.privileges || []).filter(
|
||||||
|
({ id }) => !selectionExisting.includes(id.toString())
|
||||||
|
);
|
||||||
|
onChange(user.privileges);
|
||||||
|
setSelectionExisting([]);
|
||||||
|
setToggle(!toggle);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<<
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.described}>
|
||||||
|
<h4>Current</h4>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectionExisting}
|
||||||
|
onChange={(e) => setSelectionExisting(getSelectValues(e.target))}
|
||||||
|
>
|
||||||
|
{(user.privileges || []).map(({ name, id }) => (
|
||||||
|
<option value={id} key={id}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditUserModal = ({
|
||||||
|
close,
|
||||||
|
modalRef,
|
||||||
|
privPriv,
|
||||||
|
user,
|
||||||
|
update,
|
||||||
|
}: ModalProps<{
|
||||||
|
user: UserListItem;
|
||||||
|
update: () => void;
|
||||||
|
privPriv: boolean;
|
||||||
|
}>) => {
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
const { formData, handleInputChange, handleSubmit } = useForm<
|
||||||
|
Partial<UserListItem>
|
||||||
|
>(user, async () => {
|
||||||
|
// Save privileges
|
||||||
|
if (privPriv) {
|
||||||
|
await toast.promise(
|
||||||
|
publishJSON(`/api/admin/users/${user.id}/privileges`, 'PUT', {
|
||||||
|
privileges: (user.privileges || []).map(({ id }) => id),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: 'Saving privileges...',
|
||||||
|
success: 'Privileges saved!',
|
||||||
|
error: 'Failed to save privileges.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user data
|
||||||
|
toast
|
||||||
|
.promise(publishJSON(`/api/admin/users/${user.id}`, 'PATCH', formData), {
|
||||||
|
loading: 'Saving user...',
|
||||||
|
success: 'User saved!',
|
||||||
|
error: (err) => `Saving the user failed: ${err.message}`,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data) {
|
||||||
|
close(true);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal modalRef={modalRef}>
|
||||||
|
<ModalHeader>
|
||||||
|
<h3>Edit user details</h3>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<FormWrapper>
|
||||||
|
<form ref={formRef} onSubmit={handleSubmit} autoComplete="off">
|
||||||
|
<FormControl>
|
||||||
|
<label htmlFor="display_name">Display name</label>
|
||||||
|
<input
|
||||||
|
id="display_name"
|
||||||
|
type="text"
|
||||||
|
name="display_name"
|
||||||
|
value={formData.display_name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<label htmlFor="username">Username</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<label htmlFor="email">Email</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl inline={true}>
|
||||||
|
<label htmlFor="activated">Activated</label>
|
||||||
|
<input
|
||||||
|
id="activated"
|
||||||
|
name="activated"
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.activated}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{privPriv && (
|
||||||
|
<PrivilegeEditor
|
||||||
|
user={user}
|
||||||
|
onChange={(privileges: Privilege[]) =>
|
||||||
|
(formData.privileges = privileges)
|
||||||
|
}
|
||||||
|
></PrivilegeEditor>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</FormWrapper>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onClick={() => close(true)}>Cancel</Button>
|
||||||
|
<Button onClick={() => handleSubmit()} variant="primary">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCard = ({
|
||||||
|
user,
|
||||||
|
update,
|
||||||
|
privPriv,
|
||||||
|
}: {
|
||||||
|
user: UserListItem;
|
||||||
|
update: () => void;
|
||||||
|
privPriv: boolean;
|
||||||
|
}) => (
|
||||||
<div className={styles.userCard}>
|
<div className={styles.userCard}>
|
||||||
<div className={styles.pictureWrapper}>
|
<div className={styles.pictureWrapper}>
|
||||||
{user.picture ? (
|
{user.picture ? (
|
||||||
@ -26,15 +273,84 @@ const UserCard = ({ user }: { user: UserListItem }) => (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.userInfo}>
|
<div className={styles.userInfo}>
|
||||||
<h2>
|
<div className={styles.titleWrap}>
|
||||||
{user.display_name}{' '}
|
<h2>
|
||||||
<span className={styles.username}>@{user.username}</span>
|
{user.display_name}{' '}
|
||||||
</h2>
|
<span className={styles.username}>@{user.username}</span>
|
||||||
|
</h2>
|
||||||
|
<Dropdown opens="right">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
ModalService.open(EditUserModal, { user, update, privPriv })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit user
|
||||||
|
</button>
|
||||||
|
{!user.activated && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
toast.promise(
|
||||||
|
publishJSON(`/api/admin/users/${user.id}/activation`, 'POST'),
|
||||||
|
{
|
||||||
|
loading: 'Sending email...',
|
||||||
|
success: 'Email sent!',
|
||||||
|
error: 'Failed to send activation email.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Send activation email
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{user.activated && (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
toast.promise(
|
||||||
|
publishJSON(`/api/admin/users/${user.id}/password`, 'POST'),
|
||||||
|
{
|
||||||
|
loading: 'Sending email...',
|
||||||
|
success: 'Email sent!',
|
||||||
|
error: 'Failed to send password email.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Send password email
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{user.picture && (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
toast
|
||||||
|
.promise(
|
||||||
|
publishJSON(`/api/admin/users/${user.id}/avatar`, 'DELETE'),
|
||||||
|
{
|
||||||
|
loading: 'Deleting avatar...',
|
||||||
|
success: 'Avatar deleted!',
|
||||||
|
error: 'Failed to delete user avatar.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(update)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete avatar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>UUID</dt>
|
<dt>UUID</dt>
|
||||||
<dd>{user.uuid}</dd>
|
<dd>{user.uuid}</dd>
|
||||||
<dt>Email</dt>
|
<dt>Email</dt>
|
||||||
<dd>{user.email}</dd>
|
<dd>{user.email}</dd>
|
||||||
|
{user.privileges?.length && (
|
||||||
|
<>
|
||||||
|
<dt>Privileges</dt>
|
||||||
|
<dd>
|
||||||
|
{user.privileges.map((privilege) => privilege.name).join(', ')}
|
||||||
|
</dd>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<dt>Activated</dt>
|
<dt>Activated</dt>
|
||||||
<dd>{user.activated ? 'Yes' : <b>NOT ACTIVATED</b>}</dd>
|
<dd>{user.activated ? 'Yes' : <b>NOT ACTIVATED</b>}</dd>
|
||||||
<dt>Registered</dt>
|
<dt>Registered</dt>
|
||||||
@ -47,24 +363,33 @@ const UserCard = ({ user }: { user: UserListItem }) => (
|
|||||||
const UserList = ({
|
const UserList = ({
|
||||||
pageIndex,
|
pageIndex,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
privPriv,
|
||||||
setPage,
|
setPage,
|
||||||
}: {
|
}: {
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
privPriv: boolean;
|
||||||
setPage: (page: number) => void;
|
setPage: (page: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = useSWR<PaginatedResponse<UserListItem>>(
|
const { data, mutate } = useSWR<PaginatedResponse<UserListItem>>(
|
||||||
`/api/admin/users?page=${pageIndex}${searchTerm ? `&q=${searchTerm}` : ''}`
|
`/api/admin/users?page=${pageIndex}${searchTerm ? `&q=${searchTerm}` : ''}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.userList}>
|
{data?.list?.length && (
|
||||||
{data.list.map((user) => (
|
<div className={styles.userList}>
|
||||||
<UserCard user={user} key={user.uuid} />
|
{data.list.map((user) => (
|
||||||
))}
|
<UserCard
|
||||||
<Paginator setPage={setPage} pagination={data.pagination}></Paginator>
|
user={user}
|
||||||
</div>
|
key={user.uuid}
|
||||||
|
update={mutate}
|
||||||
|
privPriv={privPriv}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Paginator setPage={setPage} pagination={data.pagination}></Paginator>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span>Nothing found</span>
|
<span>Nothing found</span>
|
||||||
@ -75,6 +400,7 @@ export const UsersPage = () => {
|
|||||||
const { user } = useUser({ redirectTo: '/login' });
|
const { user } = useUser({ redirectTo: '/login' });
|
||||||
const [pageIndex, setPageIndex] = useState(1);
|
const [pageIndex, setPageIndex] = useState(1);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const privPriv = useHasPrivileges(user, 'admin:user:privilege');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -84,6 +410,7 @@ export const UsersPage = () => {
|
|||||||
<UserList
|
<UserList
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
privPriv={privPriv}
|
||||||
setPage={setPageIndex}
|
setPage={setPageIndex}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background linear 0.33s;
|
transition: background linear 0.33s;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
&.default {
|
&.default {
|
||||||
border: 1px solid #b9b9b9;
|
border: 1px solid #b9b9b9;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
@ -13,21 +17,23 @@
|
|||||||
rgb(241 241 241) 100%
|
rgb(241 241 241) 100%
|
||||||
);
|
);
|
||||||
|
|
||||||
&:hover,
|
&:not([disabled]) {
|
||||||
&:focus-visible {
|
&:hover,
|
||||||
background: linear-gradient(
|
&:focus-visible {
|
||||||
180deg,
|
background: linear-gradient(
|
||||||
rgb(255 255 255) 0%,
|
180deg,
|
||||||
rgb(250 250 250) 100%
|
rgb(255 255 255) 0%,
|
||||||
);
|
rgb(250 250 250) 100%
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgb(241 241 241) 0%,
|
rgb(241 241 241) 0%,
|
||||||
rgb(246 246 246) 100%
|
rgb(246 246 246) 100%
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.primary {
|
&.primary {
|
||||||
@ -38,21 +44,23 @@
|
|||||||
rgba(59, 190, 255, 1) 100%
|
rgba(59, 190, 255, 1) 100%
|
||||||
);
|
);
|
||||||
|
|
||||||
&:hover,
|
&:not([disabled]) {
|
||||||
&:focus-visible {
|
&:hover,
|
||||||
background: linear-gradient(
|
&:focus-visible {
|
||||||
180deg,
|
background: linear-gradient(
|
||||||
rgb(145 220 255) 0%,
|
180deg,
|
||||||
rgb(84 198 255) 100%
|
rgb(145 220 255) 0%,
|
||||||
);
|
rgb(84 198 255) 100%
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgb(77, 196, 255) 0%,
|
rgb(77, 196, 255) 0%,
|
||||||
rgb(124, 213, 255) 100%
|
rgb(124, 213, 255) 100%
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.secondary {
|
&.secondary {
|
||||||
|
@ -10,7 +10,7 @@ export const Button = ({
|
|||||||
variant?: 'default' | 'primary' | 'secondary';
|
variant?: 'default' | 'primary' | 'secondary';
|
||||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}) => {
|
} & JSX.IntrinsicElements['button']) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input[type='text'],
|
input[type='text'],
|
||||||
|
input[type='email'],
|
||||||
input[type='password'],
|
input[type='password'],
|
||||||
input:not([type]),
|
input:not([type]),
|
||||||
textarea {
|
textarea {
|
||||||
|
@ -21,6 +21,22 @@ export default async function fetchJson<JSON = unknown>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function publishJSON(
|
||||||
|
input: RequestInfo,
|
||||||
|
method: string = 'POST',
|
||||||
|
body?: Object,
|
||||||
|
init?: RequestInit
|
||||||
|
) {
|
||||||
|
return fetchJson(input, {
|
||||||
|
...init,
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export class FetchError extends Error {
|
export class FetchError extends Error {
|
||||||
response: Response;
|
response: Response;
|
||||||
data: {
|
data: {
|
||||||
|
Reference in New Issue
Block a user