diff --git a/components/OAuth2Page/OAuth2Page.module.scss b/components/OAuth2Page/OAuth2Page.module.scss index 40fcc82..def59ab 100644 --- a/components/OAuth2Page/OAuth2Page.module.scss +++ b/components/OAuth2Page/OAuth2Page.module.scss @@ -4,7 +4,7 @@ border-radius: 8px; gap: 1rem; padding: 0.5rem; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.25); + box-shadow: 0 0 8px var(--box-shadow-color); .titleWrap { display: flex; diff --git a/components/OAuth2Page/OAuth2Page.tsx b/components/OAuth2Page/OAuth2Page.tsx index be6a2b6..4ac2989 100644 --- a/components/OAuth2Page/OAuth2Page.tsx +++ b/components/OAuth2Page/OAuth2Page.tsx @@ -27,7 +27,7 @@ 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'; -import { publishJSON } from '../../lib/utils/swr-fetcher'; +import { publishJSON } from '../../lib/utils/fetch'; const LINK_NAMES = { redirect_uri: 'Redirect URI', diff --git a/components/UsersPage/UsersPage.module.scss b/components/UsersPage/UsersPage.module.scss index f6bf6d4..4a17570 100644 --- a/components/UsersPage/UsersPage.module.scss +++ b/components/UsersPage/UsersPage.module.scss @@ -4,7 +4,7 @@ border-radius: 8px; gap: 1rem; padding: 0.5rem; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.25); + box-shadow: 0 0 8px var(--box-shadow-color); .titleWrap { display: flex; diff --git a/components/UsersPage/UsersPage.tsx b/components/UsersPage/UsersPage.tsx index 1ae7ab4..0b1403d 100644 --- a/components/UsersPage/UsersPage.tsx +++ b/components/UsersPage/UsersPage.tsx @@ -12,7 +12,7 @@ import { UPLOADS_URL } from '../../lib/constants'; 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 { publishJSON } from '../../lib/utils/fetch'; import { useForm } from '../../lib/hooks/useForm'; import { ModalProps } from '../../lib/types/modal.interface'; import { Button } from '../common/Button/Button'; diff --git a/components/common/Button/Button.module.scss b/components/common/Button/Button.module.scss index a03d5c1..9fcbc10 100644 --- a/components/common/Button/Button.module.scss +++ b/components/common/Button/Button.module.scss @@ -2,70 +2,68 @@ appearance: none; padding: 0.5rem 1.5rem; border-radius: 4px; - transition: background linear 0.33s; + transition: background linear 0.15s; + + --btn-background: transparent; + --btn-color: #000; + --btn-border: #ddd; + + color: var(--btn-color); + background: var(--btn-background); + border: 1px solid var(--btn-border); &:not([disabled]) { cursor: pointer; } &.default { - border: 1px solid #b9b9b9; - background: linear-gradient( - 180deg, - rgb(246 246 246) 0%, - rgb(241 241 241) 100% - ); + --btn-color: var(--button-default-color); + --btn-border: var(--button-default-border); + --btn-background: var(--button-default-background); + + &[disabled] { + --btn-background: var(--button-default-disabled); + --btn-color: var(--button-default-disabled-color); + } &:not([disabled]) { &:hover, &:focus-visible { - background: linear-gradient( - 180deg, - rgb(255 255 255) 0%, - rgb(250 250 250) 100% - ); + --btn-background: var(--button-default-hover); } &:active { - background: linear-gradient( - 180deg, - rgb(241 241 241) 0%, - rgb(246 246 246) 100% - ); + --btn-background: var(--button-default-active); } } } + &.primary { - border: 1px solid #00aaff; - background: linear-gradient( - 180deg, - rgb(133 216 255) 0%, - rgba(59, 190, 255, 1) 100% - ); + --btn-color: var(--button-primary-color); + --btn-border: 1px solid var(--button-primary-border); + --btn-background: var(--button-primary-background); + + &[disabled] { + --btn-background: var(--button-primary-disabled); + --btn-color: var(--button-primary-disabled-color); + } &:not([disabled]) { &:hover, &:focus-visible { - background: linear-gradient( - 180deg, - rgb(145 220 255) 0%, - rgb(84 198 255) 100% - ); + --btn-background: var(--button-primary-hover); } &:active { - background: linear-gradient( - 180deg, - rgb(77, 196, 255) 0%, - rgb(124, 213, 255) 100% - ); + --btn-background: var(--button-primary-active); } } } + &.link { - border: 0; - background: transparent; - color: #0090d8; + --btn-border: transparent; + --btn-background: transparent; + --btn-color: var(--button-link-color); &:not([disabled]) { &:hover, diff --git a/components/common/Container/Container.module.scss b/components/common/Container/Container.module.scss index 83706ad..824212c 100644 --- a/components/common/Container/Container.module.scss +++ b/components/common/Container/Container.module.scss @@ -2,6 +2,6 @@ max-width: 1080px; margin: 0 auto; padding: 1rem; - background-color: #fff; + background-color: var(--container-main); min-height: calc(100vh - 54px); } diff --git a/components/common/Dropdown/Dropdown.module.scss b/components/common/Dropdown/Dropdown.module.scss index f59686c..5299a25 100644 --- a/components/common/Dropdown/Dropdown.module.scss +++ b/components/common/Dropdown/Dropdown.module.scss @@ -3,8 +3,9 @@ .toggle { appearance: none; - border: 1px solid #ddd; - background: #ffffff; + border: 1px solid var(--container-main-border); + transition: background-color linear 0.15s; + background-color: var(--container-main); border-radius: 4px; width: 32px; height: 32px; @@ -13,7 +14,7 @@ &.active, &:hover, &:focus-visible { - background-color: rgb(240, 240, 240); + background-color: var(--container-main-secondary); } &.active { @@ -35,8 +36,8 @@ flex-direction: column; top: 100%; min-width: 140px; - background-color: #fff; - border: 1px solid #ddd; + background-color: var(--container-main); + border: 1px solid var(--container-main-border); border-radius: 4px; a, @@ -44,10 +45,11 @@ appearance: none; border: 0; background: transparent; + transition: background-color linear 0.15s; font-size: 1rem; text-align: left; padding: 0.5rem 0.5rem; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--container-main-border); cursor: pointer; &:hover, diff --git a/components/common/Form/Form.module.scss b/components/common/Form/Form.module.scss index 45536b4..4f1a164 100644 --- a/components/common/Form/Form.module.scss +++ b/components/common/Form/Form.module.scss @@ -32,13 +32,15 @@ font-size: 14px; font-family: inherit; font-weight: 400; - border: 1px solid #a4a4a4; - box-shadow: inset 0 0 4px #0000001f; + color: var(--form-field-color); + background: var(--form-field-background); + border: 1px solid var(--form-field-border); + box-shadow: inset 0 0 4px var(--form-field-box-shadow); + span { font-size: 0.875rem; margin-top: 0.25rem; - color: rgb(100, 100, 100); + color: var(--form-field-helper-color); } } diff --git a/components/common/Header/Header.module.scss b/components/common/Header/Header.module.scss index e38459e..4290802 100644 --- a/components/common/Header/Header.module.scss +++ b/components/common/Header/Header.module.scss @@ -1,13 +1,15 @@ .nav { display: flex; - background-color: #2eb9ff; - border-bottom: 4px solid #00aaff; + color: var(--nav-text-color); + background-color: var(--nav-main-color); + border-bottom: 4px solid var(--nav-secondary-color); .inner { display: flex; max-width: 1080px; width: 100%; margin: 0 auto; + ul { list-style: none; display: flex; @@ -26,7 +28,7 @@ font-weight: bold; a { - background-color: #00aaff; + background-color: var(--nav-secondary-color); } } diff --git a/components/common/Modal/Modal/Modal.module.scss b/components/common/Modal/Modal/Modal.module.scss index 8f7a323..ada321a 100644 --- a/components/common/Modal/Modal/Modal.module.scss +++ b/components/common/Modal/Modal/Modal.module.scss @@ -3,14 +3,15 @@ flex-direction: column; max-width: 480px; margin: 0 auto; - background-color: #fff; + background-color: var(--container-main); margin-top: 8%; border-radius: 8px; - box-shadow: 0 8px 32px #0000006b; + box-shadow: 0 8px 32px var(--modal-shadow-color); + color: var(--text-color-main); .header { padding: 1rem; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--container-main-border); display: flex; justify-content: space-between; @@ -29,7 +30,7 @@ .footer { padding: 1rem; - border-top: 1px solid #ddd; + border-top: 1px solid var(--container-main-border); gap: 0.5rem; display: flex; justify-content: flex-end; diff --git a/lib/api/remote.ts b/lib/api/remote.ts index 831ed1a..c06ce0b 100644 --- a/lib/api/remote.ts +++ b/lib/api/remote.ts @@ -1,8 +1,24 @@ -import { API_URL } from '../constants'; -import { CurrentUserDto } from '../types/user-response.interface'; -import { fetchBearer, fetchToken } from '../utils/fetch'; +import { API_URL, CLIENT_ID, CLIENT_SECRET, TOKEN_URL } from '../constants'; export const getUserInfo = async (accessToken: string) => - await fetchBearer(accessToken, `${API_URL}/user`); + fetch(`${API_URL}/user`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()); -export const getAccessToken = async (code: string) => await fetchToken(code); +export const getAccessToken = async (code: string) => + fetch(TOKEN_URL, { + headers: { + Authorization: `Basic ${Buffer.from( + `${CLIENT_ID}:${CLIENT_SECRET}` + ).toString('base64')}`, + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + grant_type: 'authorization_code', + code, + }), + }).then((res) => res.json()); diff --git a/lib/utils/fetch.ts b/lib/utils/fetch.ts index e26a338..3dc8c45 100644 --- a/lib/utils/fetch.ts +++ b/lib/utils/fetch.ts @@ -1,32 +1,68 @@ -import { CLIENT_ID, CLIENT_SECRET, TOKEN_URL } from '../constants'; -import { OAuth2TokenDto } from '../types/token-response.interface'; +export default async function fetchJson( + input: RequestInfo, + init?: RequestInit +): Promise { + const response = await fetch(input, { ...init, credentials: 'include' }); -export async function fetchBearer( - accessToken: string, - url: string, - ...options: any[] -): Promise { - return fetch(url, { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - ...options, - }).then((res) => res.json()); + // if the server replies, there's always some data in json + // if there's a network error, it will throw at the previous line + const data = await response.json(); + + // response.ok is true when res.status is 2xx + // https://developer.mozilla.org/en-US/docs/Web/API/Response/ok + if (response.ok) { + return data; + } + + throw new FetchError({ + message: response.statusText, + response, + data, + }); } -export async function fetchToken(code: string): Promise { - return fetch(TOKEN_URL, { +export async function publishJSON( + input: RequestInfo, + method: string = 'POST', + body?: Object, + init?: RequestInit +) { + return fetchJson(input, { + ...init, + method, headers: { - Authorization: `Basic ${Buffer.from( - `${CLIENT_ID}:${CLIENT_SECRET}` - ).toString('base64')}`, 'Content-Type': 'application/json', }, - method: 'POST', - body: JSON.stringify({ - grant_type: 'authorization_code', - code, - }), - }).then((res) => res.json()); + body: JSON.stringify(body), + }); +} + +export class FetchError extends Error { + response: Response; + data: { + message: string; + }; + constructor({ + message, + response, + data, + }: { + message: string; + response: Response; + data: { + message: string; + }; + }) { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(message); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, FetchError); + } + + this.name = 'FetchError'; + this.response = response; + this.data = data ?? { message: message }; + } } diff --git a/lib/utils/swr-fetcher.ts b/lib/utils/swr-fetcher.ts deleted file mode 100644 index 3dc8c45..0000000 --- a/lib/utils/swr-fetcher.ts +++ /dev/null @@ -1,68 +0,0 @@ -export default async function fetchJson( - input: RequestInfo, - init?: RequestInit -): Promise { - const response = await fetch(input, { ...init, credentials: 'include' }); - - // if the server replies, there's always some data in json - // if there's a network error, it will throw at the previous line - const data = await response.json(); - - // response.ok is true when res.status is 2xx - // https://developer.mozilla.org/en-US/docs/Web/API/Response/ok - if (response.ok) { - return data; - } - - throw new FetchError({ - message: response.statusText, - response, - data, - }); -} - -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 { - response: Response; - data: { - message: string; - }; - constructor({ - message, - response, - data, - }: { - message: string; - response: Response; - data: { - message: string; - }; - }) { - // Pass remaining arguments (including vendor specific ones) to parent constructor - super(message); - - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, FetchError); - } - - this.name = 'FetchError'; - this.response = response; - this.data = data ?? { message: message }; - } -} diff --git a/pages/_app.tsx b/pages/_app.tsx index 0255671..3c6bdcf 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,7 +1,7 @@ import '../styles/globals.scss'; import type { AppProps } from 'next/app'; import { SWRConfig } from 'swr'; -import fetchJson from '../lib/utils/swr-fetcher'; +import fetchJson from '../lib/utils/fetch'; import ModalRoot from '../components/common/Modal/ModalRoot/ModalRoot'; import { Toaster } from 'react-hot-toast'; diff --git a/styles/_colors.scss b/styles/_colors.scss new file mode 100644 index 0000000..7e95380 --- /dev/null +++ b/styles/_colors.scss @@ -0,0 +1,40 @@ +:root { + --text-color-main: #000; + --background-main: rgb(231, 231, 231); + --container-main: #fff; + --container-main-border: #ddd; + --container-main-secondary: rgb(240, 240, 240); + + --link-text-color: #0090d8; + + --nav-text-color: #000; + --nav-main-color: #2eb9ff; + --nav-secondary-color: #00aaff; + + --box-shadow-color: rgb(0 0 0 / 25%); + --modal-shadow-color: rgb(0 0 0 / 42%); + + --button-default-color: #000; + --button-default-border: #b9b9b9; + --button-default-background: rgb(246 246 246); + --button-default-disabled: #ebebeb; + --button-default-disabled-color: #939393; + --button-default-hover: rgb(250 250 250); + --button-default-active: rgb(226, 226, 226); + + --button-primary-color: #000; + --button-primary-border: #00aaff; + --button-primary-background: rgb(90, 200, 255); + --button-primary-disabled: rgb(39 131 177); + --button-primary-disabled-color: #484848; + --button-primary-hover: rgb(114, 208, 255); + --button-primary-active: rgb(70, 196, 255); + + --button-link-color: #0090d8; + + --form-field-color: #000; + --form-field-background: #fff; + --form-field-border: #a4a4a4; + --form-field-box-shadow: #0000001f; + --form-field-helper-color: rgb(100, 100, 100); +} diff --git a/styles/globals.scss b/styles/globals.scss index d3f44ef..87e7bf9 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -1,3 +1,4 @@ +@import 'colors'; @import 'breakpoint'; @import 'focus'; @@ -7,11 +8,12 @@ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; - background-color: rgb(231, 231, 231); + background-color: var(--background-main); + color: var(--text-color-main); } a { - color: #00aaff; + color: var(--link-text-color); text-decoration: none; &:hover { @@ -39,13 +41,3 @@ dl { margin-top: 0.5rem; } } - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } - body { - color: white; - background: black; - } -}