oauth2 client editing and adding

This commit is contained in:
Evert Prants 2022-09-01 17:23:11 +03:00
parent 3feff2a367
commit 9b9141d5a9
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
15 changed files with 640 additions and 281 deletions

View File

@ -27,6 +27,23 @@
font-size: 0.875rem;
}
}
.urls {
display: flex;
flex-direction: column;
align-items: flex-start;
}
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
h1 {
margin-bottom: 0;
}
}

View File

@ -1,4 +1,4 @@
import useSWR from 'swr';
import useSWR, { mutate } from 'swr';
import Image from 'next/image';
import {
OAuth2ClientListItem,
@ -24,6 +24,9 @@ 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';
import toast from 'react-hot-toast';
import { Button } from '../common/Button/Button';
import userHasPrivileges from '../../lib/utils/has-privileges';
const LINK_NAMES = {
redirect_uri: 'Redirect URI',
@ -59,7 +62,10 @@ const LinkEdit = ({
onChange={(e) => {
if (!formUrl.type) {
formUrl.type = linkType;
(formData.urls as Partial<OAuth2ClientURL>[])?.push(formUrl);
(formData.urls as Partial<OAuth2ClientURL>[]) = [
...(formData.urls || []),
formUrl,
];
}
formUrl.url = e.target.value;
handleInputChange(e, formData.urls, 'urls');
@ -73,31 +79,69 @@ const EditClientModal = ({
close,
modalRef,
client,
}: ModalProps<{ client?: OAuth2ClientListItem }>) => {
isAdmin,
update,
}: ModalProps<{
client?: OAuth2ClientListItem;
update: () => void;
isAdmin: boolean;
}>) => {
const formRef = useRef<HTMLFormElement>(null);
const { formData, handleInputChange, handleSubmit } = useForm<
Partial<OAuth2ClientListItem>
>(client || {}, () => {
console.log(formData);
toast
.promise(
fetch(
client
? `/api/admin/oauth2/clients/${client.id}`
: '/api/admin/oauth2/clients',
{
method: client ? 'PATCH' : 'POST',
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...',
success: 'Client saved!',
error: (err) => `Saving the client failed: ${err.message}`,
}
)
.then((data) => {
if (data) {
close(true);
update();
}
});
return false;
});
return (
<Modal modalRef={modalRef}>
<ModalHeader>
<h3>Edit OAuth2 Client</h3>
<h3>{client ? 'Edit OAuth2 Client' : 'New OAuth2 Client'}</h3>
</ModalHeader>
<ModalBody>
<FormWrapper>
<form ref={formRef} onSubmit={handleSubmit}>
<form ref={formRef} onSubmit={handleSubmit} autoComplete="off">
<FormControl>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
name="title"
value={formData.title}
value={formData.title || ''}
onChange={handleInputChange}
/>
</FormControl>
@ -106,7 +150,7 @@ const EditClientModal = ({
<textarea
id="description"
name="description"
value={formData.description}
value={formData.description || ''}
onChange={handleInputChange}
/>
</FormControl>
@ -115,7 +159,7 @@ const EditClientModal = ({
<input
id="scope"
name="scope"
value={formData.scope}
value={formData.scope || ''}
onChange={handleInputChange}
/>
</FormControl>
@ -124,48 +168,103 @@ const EditClientModal = ({
<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}
value={formData.grants || ''}
onChange={handleInputChange}
/>
</FormControl>
{isAdmin && (
<>
<FormControl inline={true}>
<label htmlFor="activated">Activated</label>
<input
id="activated"
name="activated"
type="checkbox"
checked={formData.activated ?? true}
onChange={handleInputChange}
/>
</FormControl>
<FormControl inline={true}>
<label htmlFor="verified">Verified</label>
<input
id="verified"
name="verified"
type="checkbox"
checked={formData.verified ?? false}
onChange={handleInputChange}
/>
</FormControl>
</>
)}
<LinkEdit
formData={formData}
handleInputChange={handleInputChange}
linkType={OAuth2ClientURLType.REDIRECT_URI}
></LinkEdit>
<LinkEdit
formData={formData}
handleInputChange={handleInputChange}
linkType={OAuth2ClientURLType.WEBSITE}
></LinkEdit>
<LinkEdit
formData={formData}
handleInputChange={handleInputChange}
linkType={OAuth2ClientURLType.PRIVACY}
></LinkEdit>
<LinkEdit
formData={formData}
handleInputChange={handleInputChange}
linkType={OAuth2ClientURLType.TERMS}
></LinkEdit>
</form>
</FormWrapper>
</ModalBody>
<ModalFooter>
<button onClick={() => close(true)}>Cancel</button>
<button>New secret</button>
<button onClick={() => handleSubmit()}>Submit</button>
<Button onClick={() => close(true)}>Cancel</Button>
{client && (
<Button
onClick={() =>
toast
.promise(
fetch(`/api/admin/oauth2/clients/${client!.id}/new-secret`, {
method: 'POST',
})
.then((res) => res.json())
.then(async (data) => {
if (data.error) {
throw data;
}
return data;
}),
{
loading: 'Generating new secret...',
success: 'New secret generated.',
error: 'Failed to generate new secret.',
}
)
.then(() => update())
}
>
New secret
</Button>
)}
<Button onClick={() => handleSubmit()} variant="primary">
Submit
</Button>
</ModalFooter>
</Modal>
);
};
const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => (
const OAuth2ClientCard = ({
client,
isAdmin,
update,
}: {
client: OAuth2ClientListItem;
isAdmin: boolean;
update: () => void;
}) => (
<div className={styles.clientCard}>
<div className={styles.pictureWrapper}>
{client.picture ? (
@ -185,11 +284,29 @@ const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => (
<Dropdown opens="right">
<button
onClick={() =>
ModalService.open(EditClientModal, { client: client })
ModalService.open(EditClientModal, {
client: client,
isAdmin,
update,
})
}
>
Edit client
</button>
<button
onClick={() => {
toast.promise(
navigator.clipboard.writeText(client.client_secret),
{
loading: 'Copying',
success: 'Copied to clipboard',
error: 'Copying to clipboard failed.',
}
);
}}
>
Copy secret
</button>
{!client.activated && <button>Delete client</button>}
</Dropdown>
</div>
@ -201,12 +318,43 @@ const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => (
<dd>{client.scope?.split(' ').join(', ')}</dd>
<dt>Allowed grant types</dt>
<dd>{client.grants?.split(' ').join(', ')}</dd>
<dt>Activated</dt>
<dd>{client.activated ? 'Yes' : <b>NOT ACTIVATED</b>}</dd>
<dt>Verified</dt>
<dd>{client.verified ? 'Yes' : 'No'}</dd>
{isAdmin && (
<>
<dt>Activated</dt>
<dd>{client.activated ? 'Yes' : <b>NOT ACTIVATED</b>}</dd>
<dt>Verified</dt>
<dd>{client.verified ? 'Yes' : 'No'}</dd>
</>
)}
{client.owner ? (
<>
<dt>Owner</dt>
<dd>
{client.owner.uuid} ({client.owner.username})
</dd>
</>
) : undefined}
<dt>Created</dt>
<dd>{new Date(client.created_at).toDateString()}</dd>
{client.urls?.length ? (
<>
<dt>URLs</dt>
<dd>
<div className={styles.urls}>
{client.urls.map((url) => (
<a
key={url.id}
href={url.url}
target="_blank"
rel="noreferrer"
>
{LINK_NAMES[url.type]} &lt;{url.url}&gt;
</a>
))}
</div>
</dd>
</>
) : undefined}
</dl>
</div>
</div>
@ -215,13 +363,15 @@ const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => (
const OAuth2ClientList = ({
pageIndex,
searchTerm,
isAdmin,
setPage,
}: {
pageIndex: number;
searchTerm: string;
isAdmin: boolean;
setPage: (page: number) => void;
}) => {
const { data } = useSWR<PaginatedResponse<OAuth2ClientListItem>>(
const { data, mutate } = useSWR<PaginatedResponse<OAuth2ClientListItem>>(
`/api/admin/oauth2/clients?page=${pageIndex}${
searchTerm ? `&q=${searchTerm}` : ''
}`
@ -229,9 +379,24 @@ const OAuth2ClientList = ({
return data ? (
<>
<div className={styles.header}>
<h1>OAuth2 clients</h1>
<Button
onClick={() =>
ModalService.open(EditClientModal, { isAdmin, update: mutate })
}
>
Create new
</Button>
</div>
<div className={styles.clientList}>
{data.list.map((client) => (
<OAuth2ClientCard client={client} key={client.client_id} />
<OAuth2ClientCard
client={client}
key={client.client_id}
isAdmin={isAdmin}
update={mutate}
/>
))}
{data?.pagination && (
<Paginator setPage={setPage} pagination={data.pagination}></Paginator>
@ -245,6 +410,7 @@ const OAuth2ClientList = ({
export const OAuth2Page = () => {
const { user } = useUser({ redirectTo: '/login' });
const isAdmin = userHasPrivileges(user, 'admin:oauth2');
const [pageIndex, setPageIndex] = useState(1);
const [searchTerm, setSearchTerm] = useState('');
@ -252,10 +418,10 @@ export const OAuth2Page = () => {
<>
<Header user={user}></Header>
<Container>
<h1>OAuth2 clients</h1>
<OAuth2ClientList
pageIndex={pageIndex}
searchTerm={searchTerm}
isAdmin={isAdmin}
setPage={setPageIndex}
/>
</Container>

View File

@ -0,0 +1,60 @@
.button {
appearance: none;
padding: 0.5rem 1.5rem;
border-radius: 4px;
cursor: pointer;
transition: background linear 0.33s;
&.default {
border: 1px solid #b9b9b9;
background: linear-gradient(
180deg,
rgb(246 246 246) 0%,
rgb(241 241 241) 100%
);
&:hover,
&:focus-visible {
background: linear-gradient(
180deg,
rgb(255 255 255) 0%,
rgb(250 250 250) 100%
);
}
&:active {
background: linear-gradient(
180deg,
rgb(241 241 241) 0%,
rgb(246 246 246) 100%
);
}
}
&.primary {
border: 1px solid #00aaff;
background: linear-gradient(
180deg,
rgb(133 216 255) 0%,
rgba(59, 190, 255, 1) 100%
);
&:hover,
&:focus-visible {
background: linear-gradient(
180deg,
rgb(145 220 255) 0%,
rgb(84 198 255) 100%
);
}
&:active {
background: linear-gradient(
180deg,
rgb(77, 196, 255) 0%,
rgb(124, 213, 255) 100%
);
}
}
&.secondary {
}
}

View File

@ -0,0 +1,23 @@
import { MouseEventHandler } from 'react';
import styles from './Button.module.scss';
export const Button = ({
variant = 'default',
onClick,
children,
...props
}: {
variant?: 'default' | 'primary' | 'secondary';
onClick: MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
}) => {
return (
<button
{...props}
className={[styles.button, styles[variant]].join(' ')}
onClick={onClick}
>
{children}
</button>
);
};

View File

@ -17,4 +17,25 @@
margin-left: 1rem;
}
}
label {
font-weight: 600;
}
input[type='text'],
input[type='password'],
input:not([type]),
textarea {
padding: 8px;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
font-weight: 400;
border: 1px solid #a4a4a4;
box-shadow: inset 0 0 4px #0000001f;
}
&:last-child {
margin-bottom: 0;
}
}

View File

@ -31,6 +31,8 @@
}
a {
color: inherit;
text-decoration: none;
padding: 1rem;
}
}

View File

@ -5,6 +5,8 @@
margin: 0 auto;
background-color: #fff;
margin-top: 8%;
border-radius: 8px;
box-shadow: 0 8px 32px #0000006b;
.header {
padding: 1rem;

View File

@ -46,11 +46,13 @@ export default function ModalRoot() {
}
window.addEventListener('keyup', handleEscapeKey);
document.body.style.overflow = 'hidden';
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
window.removeEventListener('keyup', handleEscapeKey);
document.body.style.overflow = '';
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};

View File

@ -9,9 +9,11 @@ export function useForm<T>(initialState: T, onSubmit: (data: T) => void) {
formField?: string
) => {
const target = e.target as HTMLInputElement;
const checkedOrValue =
target.type === 'checkbox' ? target.checked : target.value;
setFormData({
...formData,
[formField || target.name]: setValue ?? target.checked ?? target.value,
[formField || target.name]: setValue ?? checkedOrValue,
});
};

504
package-lock.json generated
View File

@ -13,6 +13,7 @@
"next": "12.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.3.0",
"swr": "^1.3.0"
},
"devDependencies": {
@ -164,6 +165,126 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@next/swc-android-arm-eabi": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.5.tgz",
"integrity": "sha512-cPWClKxGhgn2dLWnspW+7psl3MoLQUcNqJqOHk2BhNcou9ARDtC0IjQkKe5qcn9qg7I7U83Gp1yh2aesZfZJMA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-android-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.2.5.tgz",
"integrity": "sha512-vMj0efliXmC5b7p+wfcQCX0AfU8IypjkzT64GiKJD9PgiA3IILNiGJr1fw2lyUDHkjeWx/5HMlMEpLnTsQslwg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.5.tgz",
"integrity": "sha512-VOPWbO5EFr6snla/WcxUKtvzGVShfs302TEMOtzYyWni6f9zuOetijJvVh9CCTzInnXAZMtHyNhefijA4HMYLg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.5.tgz",
"integrity": "sha512-5o8bTCgAmtYOgauO/Xd27vW52G2/m3i5PX7MUYePquxXAnX73AAtqA3WgPXBRitEB60plSKZgOTkcpqrsh546A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-freebsd-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.5.tgz",
"integrity": "sha512-yYUbyup1JnznMtEBRkK4LT56N0lfK5qNTzr6/DEyDw5TbFVwnuy2hhLBzwCBkScFVjpFdfiC6SQAX3FrAZzuuw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm-gnueabihf": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.5.tgz",
"integrity": "sha512-2ZE2/G921Acks7UopJZVMgKLdm4vN4U0yuzvAMJ6KBavPzqESA2yHJlm85TV/K9gIjKhSk5BVtauIUntFRP8cg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.5.tgz",
"integrity": "sha512-/I6+PWVlz2wkTdWqhlSYYJ1pWWgUVva6SgX353oqTh8njNQp1SdFQuWDqk8LnM6ulheVfSsgkDzxrDaAQZnzjQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.5.tgz",
"integrity": "sha512-LPQRelfX6asXyVr59p5sTpx5l+0yh2Vjp/R8Wi4X9pnqcayqT4CUJLiHqCvZuLin3IsFdisJL0rKHMoaZLRfmg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.5.tgz",
@ -196,6 +317,51 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.5.tgz",
"integrity": "sha512-3/90DRNSqeeSRMMEhj4gHHQlLhhKg5SCCoYfE3kBjGpE63EfnblYUqsszGGZ9ekpKL/R4/SGB40iCQr8tR5Jiw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.5.tgz",
"integrity": "sha512-hGLc0ZRAwnaPL4ulwpp4D2RxmkHQLuI8CFOEEHdzZpS63/hMVzv81g8jzYA0UXbb9pus/iTc3VRbVbAM03SRrw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.5.tgz",
"integrity": "sha512-7h5/ahY7NeaO2xygqVrSG/Y8Vs4cdjxIjowTZ5W6CKoTKn7tmnuxlUc2h74x06FKmbhAd9agOjr/AOKyxYYm9Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -917,7 +1083,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==",
"dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@ -1848,6 +2013,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/goober": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.11.tgz",
"integrity": "sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A==",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
@ -2899,6 +3072,21 @@
"react": "^18.2.0"
}
},
"node_modules/react-hot-toast": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.3.0.tgz",
"integrity": "sha512-/RxV+bfjld7tSJR1SCLzMAXgFuNW7fCpK6+vbYqfmbGSWcqTMz2rizrvfWKvtcPH5HK0NqxmBaC5SrAy1F42zA==",
"dependencies": {
"goober": "^2.1.10"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -3487,171 +3675,6 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@next/swc-android-arm-eabi": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.5.tgz",
"integrity": "sha512-cPWClKxGhgn2dLWnspW+7psl3MoLQUcNqJqOHk2BhNcou9ARDtC0IjQkKe5qcn9qg7I7U83Gp1yh2aesZfZJMA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-android-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.2.5.tgz",
"integrity": "sha512-vMj0efliXmC5b7p+wfcQCX0AfU8IypjkzT64GiKJD9PgiA3IILNiGJr1fw2lyUDHkjeWx/5HMlMEpLnTsQslwg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.5.tgz",
"integrity": "sha512-VOPWbO5EFr6snla/WcxUKtvzGVShfs302TEMOtzYyWni6f9zuOetijJvVh9CCTzInnXAZMtHyNhefijA4HMYLg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.5.tgz",
"integrity": "sha512-5o8bTCgAmtYOgauO/Xd27vW52G2/m3i5PX7MUYePquxXAnX73AAtqA3WgPXBRitEB60plSKZgOTkcpqrsh546A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-freebsd-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.5.tgz",
"integrity": "sha512-yYUbyup1JnznMtEBRkK4LT56N0lfK5qNTzr6/DEyDw5TbFVwnuy2hhLBzwCBkScFVjpFdfiC6SQAX3FrAZzuuw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm-gnueabihf": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.5.tgz",
"integrity": "sha512-2ZE2/G921Acks7UopJZVMgKLdm4vN4U0yuzvAMJ6KBavPzqESA2yHJlm85TV/K9gIjKhSk5BVtauIUntFRP8cg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.5.tgz",
"integrity": "sha512-/I6+PWVlz2wkTdWqhlSYYJ1pWWgUVva6SgX353oqTh8njNQp1SdFQuWDqk8LnM6ulheVfSsgkDzxrDaAQZnzjQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.5.tgz",
"integrity": "sha512-LPQRelfX6asXyVr59p5sTpx5l+0yh2Vjp/R8Wi4X9pnqcayqT4CUJLiHqCvZuLin3IsFdisJL0rKHMoaZLRfmg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.5.tgz",
"integrity": "sha512-3/90DRNSqeeSRMMEhj4gHHQlLhhKg5SCCoYfE3kBjGpE63EfnblYUqsszGGZ9ekpKL/R4/SGB40iCQr8tR5Jiw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.5.tgz",
"integrity": "sha512-hGLc0ZRAwnaPL4ulwpp4D2RxmkHQLuI8CFOEEHdzZpS63/hMVzv81g8jzYA0UXbb9pus/iTc3VRbVbAM03SRrw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.5.tgz",
"integrity": "sha512-7h5/ahY7NeaO2xygqVrSG/Y8Vs4cdjxIjowTZ5W6CKoTKn7tmnuxlUc2h74x06FKmbhAd9agOjr/AOKyxYYm9Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
},
"dependencies": {
@ -3750,6 +3773,54 @@
}
}
},
"@next/swc-android-arm-eabi": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.5.tgz",
"integrity": "sha512-cPWClKxGhgn2dLWnspW+7psl3MoLQUcNqJqOHk2BhNcou9ARDtC0IjQkKe5qcn9qg7I7U83Gp1yh2aesZfZJMA==",
"optional": true
},
"@next/swc-android-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.2.5.tgz",
"integrity": "sha512-vMj0efliXmC5b7p+wfcQCX0AfU8IypjkzT64GiKJD9PgiA3IILNiGJr1fw2lyUDHkjeWx/5HMlMEpLnTsQslwg==",
"optional": true
},
"@next/swc-darwin-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.5.tgz",
"integrity": "sha512-VOPWbO5EFr6snla/WcxUKtvzGVShfs302TEMOtzYyWni6f9zuOetijJvVh9CCTzInnXAZMtHyNhefijA4HMYLg==",
"optional": true
},
"@next/swc-darwin-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.5.tgz",
"integrity": "sha512-5o8bTCgAmtYOgauO/Xd27vW52G2/m3i5PX7MUYePquxXAnX73AAtqA3WgPXBRitEB60plSKZgOTkcpqrsh546A==",
"optional": true
},
"@next/swc-freebsd-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.5.tgz",
"integrity": "sha512-yYUbyup1JnznMtEBRkK4LT56N0lfK5qNTzr6/DEyDw5TbFVwnuy2hhLBzwCBkScFVjpFdfiC6SQAX3FrAZzuuw==",
"optional": true
},
"@next/swc-linux-arm-gnueabihf": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.5.tgz",
"integrity": "sha512-2ZE2/G921Acks7UopJZVMgKLdm4vN4U0yuzvAMJ6KBavPzqESA2yHJlm85TV/K9gIjKhSk5BVtauIUntFRP8cg==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.5.tgz",
"integrity": "sha512-/I6+PWVlz2wkTdWqhlSYYJ1pWWgUVva6SgX353oqTh8njNQp1SdFQuWDqk8LnM6ulheVfSsgkDzxrDaAQZnzjQ==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.5.tgz",
"integrity": "sha512-LPQRelfX6asXyVr59p5sTpx5l+0yh2Vjp/R8Wi4X9pnqcayqT4CUJLiHqCvZuLin3IsFdisJL0rKHMoaZLRfmg==",
"optional": true
},
"@next/swc-linux-x64-gnu": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.5.tgz",
@ -3762,6 +3833,24 @@
"integrity": "sha512-zg/Y6oBar1yVnW6Il1I/08/2ukWtOG6s3acdJdEyIdsCzyQi4RLxbbhkD/EGQyhqBvd3QrC6ZXQEXighQUAZ0g==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.5.tgz",
"integrity": "sha512-3/90DRNSqeeSRMMEhj4gHHQlLhhKg5SCCoYfE3kBjGpE63EfnblYUqsszGGZ9ekpKL/R4/SGB40iCQr8tR5Jiw==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.5.tgz",
"integrity": "sha512-hGLc0ZRAwnaPL4ulwpp4D2RxmkHQLuI8CFOEEHdzZpS63/hMVzv81g8jzYA0UXbb9pus/iTc3VRbVbAM03SRrw==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.5.tgz",
"integrity": "sha512-7h5/ahY7NeaO2xygqVrSG/Y8Vs4cdjxIjowTZ5W6CKoTKn7tmnuxlUc2h74x06FKmbhAd9agOjr/AOKyxYYm9Q==",
"optional": true
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4273,8 +4362,7 @@
"csstype": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==",
"dev": true
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
},
"damerau-levenshtein": {
"version": "1.0.8",
@ -4931,6 +5019,12 @@
"slash": "^3.0.0"
}
},
"goober": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.11.tgz",
"integrity": "sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A==",
"requires": {}
},
"grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
@ -5603,6 +5697,14 @@
"scheduler": "^0.23.0"
}
},
"react-hot-toast": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.3.0.tgz",
"integrity": "sha512-/RxV+bfjld7tSJR1SCLzMAXgFuNW7fCpK6+vbYqfmbGSWcqTMz2rizrvfWKvtcPH5HK0NqxmBaC5SrAy1F42zA==",
"requires": {
"goober": "^2.1.10"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -5982,72 +6084,6 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"@next/swc-android-arm-eabi": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.5.tgz",
"integrity": "sha512-cPWClKxGhgn2dLWnspW+7psl3MoLQUcNqJqOHk2BhNcou9ARDtC0IjQkKe5qcn9qg7I7U83Gp1yh2aesZfZJMA==",
"optional": true
},
"@next/swc-android-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.2.5.tgz",
"integrity": "sha512-vMj0efliXmC5b7p+wfcQCX0AfU8IypjkzT64GiKJD9PgiA3IILNiGJr1fw2lyUDHkjeWx/5HMlMEpLnTsQslwg==",
"optional": true
},
"@next/swc-darwin-arm64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.5.tgz",
"integrity": "sha512-VOPWbO5EFr6snla/WcxUKtvzGVShfs302TEMOtzYyWni6f9zuOetijJvVh9CCTzInnXAZMtHyNhefijA4HMYLg==",
"optional": true
},
"@next/swc-darwin-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.5.tgz",
"integrity": "sha512-5o8bTCgAmtYOgauO/Xd27vW52G2/m3i5PX7MUYePquxXAnX73AAtqA3WgPXBRitEB60plSKZgOTkcpqrsh546A==",
"optional": true
},
"@next/swc-freebsd-x64": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.5.tgz",
"integrity": "sha512-yYUbyup1JnznMtEBRkK4LT56N0lfK5qNTzr6/DEyDw5TbFVwnuy2hhLBzwCBkScFVjpFdfiC6SQAX3FrAZzuuw==",
"optional": true
},
"@next/swc-linux-arm-gnueabihf": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.5.tgz",
"integrity": "sha512-2ZE2/G921Acks7UopJZVMgKLdm4vN4U0yuzvAMJ6KBavPzqESA2yHJlm85TV/K9gIjKhSk5BVtauIUntFRP8cg==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.5.tgz",
"integrity": "sha512-/I6+PWVlz2wkTdWqhlSYYJ1pWWgUVva6SgX353oqTh8njNQp1SdFQuWDqk8LnM6ulheVfSsgkDzxrDaAQZnzjQ==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.5.tgz",
"integrity": "sha512-LPQRelfX6asXyVr59p5sTpx5l+0yh2Vjp/R8Wi4X9pnqcayqT4CUJLiHqCvZuLin3IsFdisJL0rKHMoaZLRfmg==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.5.tgz",
"integrity": "sha512-3/90DRNSqeeSRMMEhj4gHHQlLhhKg5SCCoYfE3kBjGpE63EfnblYUqsszGGZ9ekpKL/R4/SGB40iCQr8tR5Jiw==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.5.tgz",
"integrity": "sha512-hGLc0ZRAwnaPL4ulwpp4D2RxmkHQLuI8CFOEEHdzZpS63/hMVzv81g8jzYA0UXbb9pus/iTc3VRbVbAM03SRrw==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
"version": "12.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.5.tgz",
"integrity": "sha512-7h5/ahY7NeaO2xygqVrSG/Y8Vs4cdjxIjowTZ5W6CKoTKn7tmnuxlUc2h74x06FKmbhAd9agOjr/AOKyxYYm9Q==",
"optional": true
}
}
}

View File

@ -14,6 +14,7 @@
"next": "12.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.3.0",
"swr": "^1.3.0"
},
"devDependencies": {

View File

@ -3,6 +3,7 @@ import type { AppProps } from 'next/app';
import { SWRConfig } from 'swr';
import fetchJson from '../lib/utils/swr-fetcher';
import ModalRoot from '../components/common/Modal/ModalRoot/ModalRoot';
import { Toaster } from 'react-hot-toast';
function MyApp({ Component, pageProps }: AppProps) {
return (
@ -14,6 +15,7 @@ function MyApp({ Component, pageProps }: AppProps) {
},
}}
>
<Toaster position="top-right" />
<Component {...pageProps} />
<ModalRoot />
</SWRConfig>

8
styles/_focus.scss Normal file
View File

@ -0,0 +1,8 @@
input,
button,
textarea,
a {
&:focus {
outline: 4px solid #94cfff9c;
}
}

View File

@ -1,4 +1,5 @@
@import 'breakpoint';
@import 'focus';
html,
body {
@ -10,8 +11,12 @@ body {
}
a {
color: inherit;
color: #00aaff;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
* {

View File

@ -479,7 +479,7 @@
"shebang-command" "^2.0.0"
"which" "^2.0.1"
"csstype@^3.0.2":
"csstype@^3.0.10", "csstype@^3.0.2":
"integrity" "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
"resolved" "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz"
"version" "3.1.0"
@ -1005,6 +1005,11 @@
"merge2" "^1.4.1"
"slash" "^3.0.0"
"goober@^2.1.10":
"integrity" "sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A=="
"resolved" "https://registry.npmjs.org/goober/-/goober-2.1.11.tgz"
"version" "2.1.11"
"grapheme-splitter@^1.0.4":
"integrity" "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
"resolved" "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz"
@ -1555,7 +1560,7 @@
"resolved" "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
"version" "1.2.3"
"react-dom@^17.0.2 || ^18.0.0-0", "react-dom@18.2.0":
"react-dom@^17.0.2 || ^18.0.0-0", "react-dom@>=16", "react-dom@18.2.0":
"integrity" "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
"version" "18.2.0"
@ -1563,12 +1568,19 @@
"loose-envify" "^1.1.0"
"scheduler" "^0.23.0"
"react-hot-toast@^2.3.0":
"integrity" "sha512-/RxV+bfjld7tSJR1SCLzMAXgFuNW7fCpK6+vbYqfmbGSWcqTMz2rizrvfWKvtcPH5HK0NqxmBaC5SrAy1F42zA=="
"resolved" "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.3.0.tgz"
"version" "2.3.0"
dependencies:
"goober" "^2.1.10"
"react-is@^16.13.1":
"integrity" "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"resolved" "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
"version" "16.13.1"
"react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2 || ^18.0.0-0", "react@^18.2.0", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", "react@18.2.0":
"react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2 || ^18.0.0-0", "react@^18.2.0", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", "react@>=16", "react@18.2.0":
"integrity" "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="
"resolved" "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
"version" "18.2.0"