diff --git a/package.json b/package.json index aeff03afe0..4e334c86d3 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@spacecowmedia/spellbook-client": "^3.14.6", "canvas": "^2.11.2", + "cookies-next": "^5.0.2", "debounce": "^2.2.0", "markdown-to-jsx": "^7.7.2", "next": "^15.0.3", @@ -31,7 +32,6 @@ "pluralize": "^8.0.0", "react": "^18.3.1", "react-confirm-alert": "^3.0.6", - "react-cookie": "^7.2.2", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", "react-select": "^5.9.0", @@ -72,4 +72,4 @@ "typescript": "^5.6.3", "wait-on": "^8.0.1" } -} \ No newline at end of file +} diff --git a/src/assets/globals.scss b/src/assets/globals.scss index 678e70b9e4..94e04ba44a 100644 --- a/src/assets/globals.scss +++ b/src/assets/globals.scss @@ -3,7 +3,7 @@ @tailwind utilities; html { - @apply bg-white font-body; + @apply bg-white font-body dark:bg-black dark:text-white; } body, @@ -18,7 +18,7 @@ html, } a { - @apply underline text-link; + @apply underline text-link dark:text-primary; } #main:focus { @@ -57,7 +57,7 @@ a { } .heading-title { - @apply font-title text-center text-4xl text-dark uppercase; + @apply font-title text-center text-4xl text-dark uppercase dark:text-light; } .heading-subtitle { @@ -65,7 +65,11 @@ a { } .button { - @apply m-4 inline-block py-2 px-3 bg-transparent text-link border-2 border-primary rounded-sm no-underline; + @apply m-4 inline-block py-2 px-3 bg-transparent text-link border-2 border-primary rounded-sm no-underline dark:text-primary; +} + +input, textarea, select, option, .input, .inputControl { + @apply border-dark bg-white dark:bg-dark dark:text-light; } .button.tight { diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 540d0aaa39..0c60008b99 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -3,9 +3,10 @@ import Link from 'next/link'; import styles from './searchBar.module.scss'; import { NextRouter, useRouter } from 'next/router'; import UserDropdown from '../layout/UserDropdown/UserDropdown'; -import { useCookies } from 'react-cookie'; import { apiConfiguration } from 'services/api.service'; import { VariantsApi } from '@spacecowmedia/spellbook-client'; +import ThemeSelector from 'components/ui/ThemeSelector/ThemeSelector'; +import CookieService from 'services/cookie.service'; type Props = { onHomepage?: boolean; @@ -44,7 +45,6 @@ const SearchBar: React.FC = ({ onHomepage, className }) => { const [mobileMenuIsOpen, setMobileMenuIsOpen] = useState(false); const [inputValue, setInputValue] = useState(getQueryFromRouter(router)); - const [cookies, setCookies] = useCookies(['variantCount']); const [variantCount, setVariantCount] = useState(initialCount); const handleSubmit = (e: FormEvent) => { e.preventDefault(); @@ -73,14 +73,16 @@ const SearchBar: React.FC = ({ onHomepage, className }) => { }, [router.query.q]); useEffect(() => { - if (cookies.variantCount) { - setVariantCount(cookies.variantCount); - handleCountUp(cookies.variantCount); - } else if (!cookies.variantCount) { + const variantCountCookie = CookieService.get('variantCount'); + const variantCount = variantCountCookie ? parseInt(variantCountCookie) : undefined; + if (variantCount) { + setVariantCount(variantCount); + handleCountUp(variantCount); + } else if (!variantCount) { variantsApi .variantsList({ limit: 1, q: 'legal:commander' }) .then((response) => { - setCookies('variantCount', response.count, { path: '/', maxAge: 3 * 60 * 60 }); + CookieService.set('variantCount', response.count, 'hours'); setVariantCount(response.count); handleCountUp(response.count); }) @@ -119,7 +121,7 @@ const SearchBar: React.FC = ({ onHomepage, className }) => { {!onHomepage && ( -
+
+ + + -
+
Cards in combo: {combo.uses.map(({ card, quantity }) => ( @@ -105,16 +105,16 @@ export const ComboResult: React.FC = ({ ))} {combo.requires.length > 0 && ( -
- +
+ +{combo.requires.reduce((q, r) => q + r.quantity, 0)} other card {combo.requires.reduce((q, r) => q + r.quantity, 0) > 1 ? 's' : ''}
)} {prereqCount > 0 && ( -
- +
+ +{prereqCount} other prerequisite{prereqCount > 1 ? 's' : ''}
@@ -133,7 +133,7 @@ export const ComboResult: React.FC = ({
{!hideVariants && combo.variantCount > 1 && ( -
+
+ {combo.variantCount - 1} variant{combo.variantCount > 2 ? 's' : ''} diff --git a/src/components/search/ComboResults/comboResults.module.scss b/src/components/search/ComboResults/comboResults.module.scss index 54e546ef2d..c323e6ffec 100644 --- a/src/components/search/ComboResults/comboResults.module.scss +++ b/src/components/search/ComboResults/comboResults.module.scss @@ -2,13 +2,25 @@ @apply flex flex-wrap justify-center; a { - @apply no-underline text-dark; + @apply no-underline text-dark dark:text-light; } .comboResult { @apply max-w-lg mx-0 my-2 rounded border-2 border-dark flex-grow flex flex-col content-center; } + .comboResultSection { + @apply border-b-2 border-light dark:border-dark; + } + + .prerequisites span { + @apply text-gray-500 dark:text-gray-400; + } + + .variantBanner { + @apply w-full bg-pink-300 text-right dark:bg-link; + } + .cardName { text-indent: -1em; margin-left: 1em; diff --git a/src/components/submission/CardSubmission/CardSubmission.tsx b/src/components/submission/CardSubmission/CardSubmission.tsx index 33190a9692..eca0b331c5 100644 --- a/src/components/submission/CardSubmission/CardSubmission.tsx +++ b/src/components/submission/CardSubmission/CardSubmission.tsx @@ -72,7 +72,6 @@ const CardSubmission = ({ card, template, onChange, index, onDelete }: Props) => placeholder="Search for a template (ex: 'Creature with haste') or type in a new one..." // hasError={!!input.error} useValueForInput - matchAgainstOptionLabel maxLength={256} /> @@ -91,7 +90,6 @@ const CardSubmission = ({ card, template, onChange, index, onDelete }: Props) => placeholder="Search for a card..." // hasError={!!input.error} useValueForInput - matchAgainstOptionLabel maxLength={256} /> @@ -114,6 +112,17 @@ const CardSubmission = ({ card, template, onChange, index, onDelete }: Props) => value={cardOrTemplate.zoneLocations.map( (zone) => ZONE_OPTIONS.find((z) => z.value === zone) || { value: 'N/A', label: 'N/A' }, )} + className="inputControl" + styles={{ + control: (base) => ({ + ...base, + background: 'inherit', + }), + menu: (base) => ({ + ...base, + background: 'inherit', + }), + }} />
diff --git a/src/components/submission/Feature Submission/FeatureSubmission.tsx b/src/components/submission/Feature Submission/FeatureSubmission.tsx index af3bba5c76..10606e661b 100644 --- a/src/components/submission/Feature Submission/FeatureSubmission.tsx +++ b/src/components/submission/Feature Submission/FeatureSubmission.tsx @@ -30,7 +30,6 @@ const FeatureSubmission: React.FC = ({ feature, onChange, onDelete, index placeholder="Search for a feature (ex: 'Infinite mana')..." // hasError={!!input.error} useValueForInput - matchAgainstOptionLabel maxLength={256} /> - diff --git a/src/pages/api/combo/[id]/generate-image.ts b/src/pages/api/combo/[id]/generate-image.ts index 6fc5db4f17..bb267fd82d 100644 --- a/src/pages/api/combo/[id]/generate-image.ts +++ b/src/pages/api/combo/[id]/generate-image.ts @@ -2,17 +2,9 @@ import { Canvas, CanvasRenderingContext2D, createCanvas, loadImage } from 'canva import { VariantsApi } from '@spacecowmedia/spellbook-client'; import { apiConfiguration } from 'services/api.service'; import { NextApiRequest, NextApiResponse } from 'next'; +import scryfall from 'scryfall-client'; import serverPath from 'lib/serverPath'; -const manaSymbols: { [key: string]: string } = { - W: 'https://svgs.scryfall.io/card-symbols/W.svg', - U: 'https://svgs.scryfall.io/card-symbols/U.svg', - B: 'https://svgs.scryfall.io/card-symbols/B.svg', - R: 'https://svgs.scryfall.io/card-symbols/R.svg', - G: 'https://svgs.scryfall.io/card-symbols/G.svg', - C: 'https://svgs.scryfall.io/card-symbols/C.svg', -}; - const width = 1080; const manaOffset = width / 25; const iWidth = (width * 32) / 500; @@ -34,7 +26,7 @@ async function headerCanvas(identityArray: any[]) { let startManaPos = width / 2 - ((identityArray.length - 1) * (iWidth + manaOffset) + iWidth) / 2; for (let [index, letter] of identityArray.entries()) { let position = index * (iWidth + manaOffset) + startManaPos; - let img = await loadImage(manaSymbols[letter]); + let img = await loadImage(scryfall.getSymbolUrl(letter)); ctx.drawImage(img, position, manaOffset / 2, iWidth, iWidth); } return canvas1; diff --git a/src/pages/api/set-theme.ts b/src/pages/api/set-theme.ts new file mode 100644 index 0000000000..a33bc94030 --- /dev/null +++ b/src/pages/api/set-theme.ts @@ -0,0 +1,16 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import CookieService from 'services/cookie.service'; +import { DARK_THEME, LIGHT_THEME, SYSTEM_THEME, THEME_COOKIE_NAME } from 'services/theme.service'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const mode = req.query.theme; + const modes = [SYSTEM_THEME, LIGHT_THEME, DARK_THEME]; + if (typeof mode !== 'string' || !modes.includes(mode)) { + return res.status(400).json({ error: 'Invalid mode' }); + } + CookieService.set(THEME_COOKIE_NAME, mode, 'year', { + req: req, + res: res, + }); + res.status(200).json({ mode }); +} diff --git a/src/pages/combo/[id]/combo.module.scss b/src/pages/combo/[id]/combo.module.scss index 29aa26bee3..dab898912b 100644 --- a/src/pages/combo/[id]/combo.module.scss +++ b/src/pages/combo/[id]/combo.module.scss @@ -22,7 +22,7 @@ } thead { - @apply text-dark border-b-4; + @apply text-dark border-b-4 dark:text-light; } tbody { diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6deeb45604..dba6c08915 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -62,7 +62,7 @@ const Home: React.FC = ({ featuredComboButtonText }) => {
-

+

The Search Engine for EDH Combos

diff --git a/src/pages/privacy-policy.module.scss b/src/pages/privacy-policy.module.scss index 0d2b6785da..9f690f8f2e 100644 --- a/src/pages/privacy-policy.module.scss +++ b/src/pages/privacy-policy.module.scss @@ -24,7 +24,7 @@ } table tr:nth-child(even) { - @apply bg-gray-100; + @apply bg-gray-100 dark:bg-gray-800; } table td { diff --git a/src/pages/report-error.module.scss b/src/pages/report-error.module.scss index e978f7e52c..bb7eed7760 100644 --- a/src/pages/report-error.module.scss +++ b/src/pages/report-error.module.scss @@ -1,5 +1,5 @@ .reportErrorContainer { pre { - @apply my-4 bg-gray-200 p-4 overflow-x-auto; + @apply my-4 bg-gray-200 p-4 overflow-x-auto dark:bg-gray-800; } } diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 049f0f5dc1..9e4b1d5071 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -157,7 +157,6 @@ const Search: React.FC = ({ combos, count, page, bannedCombos, error, fea id="sort-combos-select" onChange={handleSortChange} selectBackgroundClassName="border-dark border-2 my-2 sm:mr-2" - selectTextClassName="text-dark" label="Change how combos are sorted" options={SORT_OPTIONS} /> @@ -169,7 +168,6 @@ const Search: React.FC = ({ combos, count, page, bannedCombos, error, fea value={order} onChange={handleOrderChange} selectBackgroundClassName="border-dark border-2 sm:m-2" - selectTextClassName="text-dark" label="Change sort direction, ascending or descending" options={ORDER_OPTIONS} /> @@ -181,7 +179,6 @@ const Search: React.FC = ({ combos, count, page, bannedCombos, error, fea value={groupBy ? 'true' : 'false'} onChange={handleGroupByComboChange} selectBackgroundClassName="border-dark border-2 my-2 sm:mr-2" - selectTextClassName="text-dark" label="Group variants by combo" options={[ { value: 'true', label: 'Yes' }, diff --git a/src/pages/style-guide.module.scss b/src/pages/style-guide.module.scss index bd330d4a99..ead42dd93b 100644 --- a/src/pages/style-guide.module.scss +++ b/src/pages/style-guide.module.scss @@ -20,6 +20,6 @@ } code { - @apply bg-gray-200 px-1 mx-1; + @apply bg-gray-200 px-1 mx-1 dark:bg-gray-700; } } diff --git a/src/pages/submit-a-combo.tsx b/src/pages/submit-a-combo.tsx index 9e39172af2..52f325f7c6 100644 --- a/src/pages/submit-a-combo.tsx +++ b/src/pages/submit-a-combo.tsx @@ -297,7 +297,7 @@ const SubmitACombo: React.FC = () => { value={manaCost} onChange={(e) => setManaCost(e.target.value)} /> -
+
Preview:
diff --git a/src/pages/syntax-guide.module.scss b/src/pages/syntax-guide.module.scss index 6bcd20ef44..319f9e2e5e 100644 --- a/src/pages/syntax-guide.module.scss +++ b/src/pages/syntax-guide.module.scss @@ -1,6 +1,6 @@ .syntaxGuideContainer { code { - @apply bg-gray-200 text-dark px-1; + @apply bg-gray-200 text-dark px-1 dark:bg-gray-700 dark:text-light; } ul { @@ -20,7 +20,7 @@ .searchGuideContainer { code { - @apply bg-gray-200 pl-1 pr-1; + @apply pl-1 pr-1; } .description p { @apply mb-4; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 44c6ae39fe..e80e63eae5 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1,21 +1,16 @@ import { GetServerSidePropsContext } from 'next'; -import CookieService from './cookie.service'; -import { Cookies } from 'react-cookie'; import tokenService from './token.service'; import { Configuration, HTTPHeaders } from '@spacecowmedia/spellbook-client'; export function apiConfiguration(serverContext?: GetServerSidePropsContext) { - const _cookies = serverContext ? new Cookies(serverContext.req.cookies) : CookieService; - const headers: HTTPHeaders = {}; + if (serverContext && serverContext.req.headers['x-forwarded-for']) { if (typeof serverContext.req.headers['x-forwarded-for'] === 'string') { headers['x-forwarded-for'] = serverContext.req.headers['x-forwarded-for']; } headers['x-forwarded-for'] = serverContext.req.headers['x-forwarded-for'][0]; } - - // TODO: refactor to get jwt from cookies return new Configuration({ basePath: process.env.NEXT_PUBLIC_EDITOR_BACKEND_URL, accessToken: function (_name?: string, _scopes?: string[]) { diff --git a/src/services/cookie.service.ts b/src/services/cookie.service.ts index 194e6e9956..613ff915bb 100644 --- a/src/services/cookie.service.ts +++ b/src/services/cookie.service.ts @@ -1,40 +1,35 @@ -import { NextPageContext } from 'next'; -import Cookies from 'universal-cookie'; +import { deleteCookie, getCookie, OptionsType, setCookie } from 'cookies-next'; export const expirationDurations = { hour: 3600, + hours: 10800, day: 86400, week: 604800, month: 2592000, year: 31536000, }; -export function get(path: string, serverCookies?: Record | string): T { - const cookies = new Cookies(serverCookies); - +export function get(path: string, options?: OptionsType): T | undefined { // @ts-ignore - const result = cookies.get(path, { path: '/' }); + const result = getCookie(path, { path: '/', ...options }); return result as T; } -export function set(key: string, value: any, age?: keyof typeof expirationDurations, cookies?: Cookies) { - const cookiesInstance = cookies ?? new Cookies(); - +export function set(key: string, value: any, age?: keyof typeof expirationDurations, options?: OptionsType) { const maxAge = age ? expirationDurations[age] : undefined; - cookiesInstance.set(key, value, { + setCookie(key, value, { path: '/', maxAge, sameSite: 'strict', httpOnly: false, + ...options, }); } -export function remove(key: string) { - const cookies = new Cookies(); - - cookies.remove(key, { path: '/' }); +export function remove(key: string, options?: OptionsType) { + deleteCookie(key, { path: '/', ...options }); } export function logout() { @@ -45,21 +40,11 @@ export function logout() { remove('csbIsStaff'); } -export function serverLogout(ctx: NextPageContext) { - ctx.res?.setHeader('Set-Cookie', [ - 'csbRefresh=deleted; path=/; Max-Age=0', - 'csbJwt=deleted; path=/; Max-Age=0', - 'csbUsername=deleted; path=/; Max-Age=0', - 'csbUserId=deleted; path=/; Max-Age=0', - ]); -} - -const cookieService = { +const CookieService = { get, set, remove, logout, - serverLogout, }; -export default cookieService; +export default CookieService; diff --git a/src/services/theme.service.ts b/src/services/theme.service.ts new file mode 100644 index 0000000000..e01e03b474 --- /dev/null +++ b/src/services/theme.service.ts @@ -0,0 +1,78 @@ +import CookieService from './cookie.service'; + +export const SYSTEM_THEME = 'system'; +export const LIGHT_THEME = 'light'; +export const DARK_THEME = 'dark'; +export const THEME_COOKIE_NAME = 'theme'; + +const themes = [LIGHT_THEME, DARK_THEME, SYSTEM_THEME]; + +export const getTheme = () => { + let localStorageTheme = localStorage.getItem(THEME_COOKIE_NAME); + if (localStorageTheme) { + if (themes.includes(localStorageTheme)) { + return localStorageTheme; + } + localStorageTheme = null; + } + let cookieTheme = CookieService.get(THEME_COOKIE_NAME); + if (cookieTheme) { + if (themes.includes(cookieTheme)) { + localStorage.setItem(THEME_COOKIE_NAME, cookieTheme); + return cookieTheme; + } + cookieTheme = undefined; + } + localStorage.setItem(THEME_COOKIE_NAME, SYSTEM_THEME); + return SYSTEM_THEME; +}; + +function applyTheme(theme: string) { + if (theme === SYSTEM_THEME) { + document.documentElement.classList.toggle(DARK_THEME, window.matchMedia('(prefers-color-scheme: dark)').matches); + } else { + document.documentElement.classList.toggle(DARK_THEME, theme === DARK_THEME); + } +} + +export const setTheme = (theme: string) => { + if (!themes.includes(theme)) { + theme = SYSTEM_THEME; + } + applyTheme(theme); + localStorage.setItem(THEME_COOKIE_NAME, theme); + fetch('/api/set-theme?theme=' + theme); +}; + +const listenThemeChanges = (): (() => void) => { + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + // Step 1. align settings with display + const initialTheme = getTheme(); + applyTheme(initialTheme); + + // Step 2. listen for changes in local storage + const storageEventListener = (e: StorageEvent) => { + if (e.key === THEME_COOKIE_NAME && e.newValue && themes.includes(e.newValue)) { + applyTheme(e.newValue); + } + }; + window.addEventListener('storage', storageEventListener); + + // Step 3. listen for changes in system preference + function handlePreferenceChange(e: MediaQueryListEvent) { + if (getTheme() === SYSTEM_THEME) { + document.documentElement.classList.toggle(DARK_THEME, e.matches); + } + } + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handlePreferenceChange); + + // Step 4. return cleanup function + return () => { + window.removeEventListener('storage', storageEventListener); + window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', handlePreferenceChange); + }; + } + return () => {}; +}; + +listenThemeChanges(); diff --git a/src/services/token.service.ts b/src/services/token.service.ts index a1af14046d..1966cc3a0c 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,6 +1,8 @@ -import { GetServerSidePropsContext, NextPageContext } from 'next'; +import { GetServerSidePropsContext } from 'next'; import CookieService from './cookie.service'; -import { Cookies } from 'react-cookie'; +import { getCookies } from 'cookies-next'; +import { apiConfiguration } from './api.service'; +import { TokenApi, TokenObtainPair } from '@spacecowmedia/spellbook-client'; export function timeInSecondsToEpoch(): number { return Math.round(Date.now() / 1000); @@ -15,12 +17,7 @@ export type DecodedJWTType = { token_type?: string; }; -type RefreshResponse = { - refresh: string; - access: string; -}; - -function decodeJwt(jwt: string): DecodedJWTType | null { +function decodeJwt(jwt?: string): DecodedJWTType | null { if (!jwt) { return null; } @@ -46,13 +43,13 @@ async function getToken(): Promise { const refreshToken = CookieService.get('csbRefresh') || null; const jwt = CookieService.get('csbJwt') || null; - if (!jwt && !refreshToken) { - return ''; - } - - if (!jwt && refreshToken) { - const result = await fetchNewToken(); - return setToken(result); + if (!jwt) { + if (!refreshToken) { + return ''; + } else { + const result = await fetchNewToken(); + return setToken(result); + } } const decodedToken = decodeJwt(jwt); @@ -72,20 +69,18 @@ async function getToken(): Promise { return setToken(result); } -async function getTokenFromServerContext( - serverContext?: GetServerSidePropsContext, - pageContext?: NextPageContext, -): Promise { - const jwt = CookieService.get('csbJwt', serverContext?.req.cookies || pageContext?.req?.headers?.cookie); - const refreshToken = CookieService.get('csbRefresh', serverContext?.req.cookies || pageContext?.req?.headers?.cookie); +async function getTokenFromServerContext(serverContext?: GetServerSidePropsContext): Promise { + const cookies = await getCookies({ ...serverContext }); + const jwt = cookies?.csbJwt; + const refreshToken = cookies?.csbRefresh; - if (!jwt && !refreshToken) { - return Promise.resolve(''); - } - - if (!jwt && refreshToken) { - const r = await fetchNewToken(refreshToken); - return setToken(r, serverContext); + if (!jwt) { + if (!refreshToken) { + return Promise.resolve(''); + } else { + const r = await fetchNewToken(refreshToken); + return setToken(r, serverContext); + } } const decodedToken = decodeJwt(jwt); @@ -104,46 +99,46 @@ async function getTokenFromServerContext( return setToken(result, serverContext); } -function setToken({ access, refresh }: RefreshResponse, serverContext?: GetServerSidePropsContext) { +function setToken({ access, refresh }: TokenObtainPair, serverContext?: GetServerSidePropsContext) { const jwt = access; - const cookies = new Cookies(); - CookieService.set('csbJwt', jwt, 'day', cookies); + CookieService.set('csbJwt', jwt, 'day', { req: serverContext?.req, res: serverContext?.res }); if (refresh) { - CookieService.set('csbRefresh', refresh, 'day', cookies); - } - const cookiesDict = cookies.getAll() as Record; - const cookieValues = []; - for (const key in cookiesDict) { - cookieValues.push(`${key}=${cookiesDict[key]}; Path=/; Max-Age={expirationDurations.day}; SameSite=Strict`); - serverContext?.res.setHeader('Set-Cookie', cookieValues); + CookieService.set('csbRefresh', refresh, 'day', { req: serverContext?.req, res: serverContext?.res }); } return jwt; } -async function fetchNewToken(providedRefreshToken?: string) { - // TODO: use spellbook client to fetch new token - const refreshToken = providedRefreshToken ? providedRefreshToken : CookieService.get('csbRefresh') || null; +async function fetchNewToken( + providedRefreshToken?: string, + serverContext?: GetServerSidePropsContext, +): Promise { + const refreshToken = providedRefreshToken + ? providedRefreshToken + : CookieService.get('csbRefresh', { req: serverContext?.req, res: serverContext?.res }) || null; - console.log('fetching new token'); + if (!refreshToken) { + CookieService.logout(); + return { access: '', refresh: '' }; + } + + const configuration = apiConfiguration(serverContext); + const tokensApi = new TokenApi(configuration); try { - const response = await fetch(`${process.env.NEXT_PUBLIC_EDITOR_BACKEND_URL}/token/refresh/`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', + const response = await tokensApi.tokenRefreshCreate({ + tokenRefreshRequest: { + refresh: refreshToken, }, - body: JSON.stringify({ refresh: refreshToken }), }); - if (!response.ok) { - throw new Error('Refresh fetch failed'); - } - return response.json(); + return { + refresh: refreshToken, + ...response, + }; } catch (_err) { CookieService.logout(); - return ''; + return { access: '', refresh: '' }; } } diff --git a/tailwind.config.js b/tailwind.config.js index cf9ea021b6..fc0b1165bf 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,23 +1,21 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx}", - "./src/components/**/*.{js,ts,jsx,tsx}", - ], + content: ['./src/pages/**/*.{js,ts,jsx,tsx}', './src/components/**/*.{js,ts,jsx,tsx}'], + darkMode: ['selector', '[class~="dark"]'], theme: { extend: { colors: { - primary: "#C18AFF", - secondary: "#FF9595", - link: "#5b2499", - dark: "#222222", - light: "#DFDFDF", - danger: "#C54329", - warning: "#FFC580", + primary: '#C18AFF', + secondary: '#FF9595', + link: '#5b2499', + dark: '#222222', + light: '#DFDFDF', + danger: '#C54329', + warning: '#FFC580', }, fontFamily: { title: "'Josefin Sans', sans-serif", - body: "Roboto, sans-serif", + body: 'Roboto, sans-serif', }, }, }, diff --git a/yarn.lock b/yarn.lock index 45d84ca4ce..86f4c46833 100644 --- a/yarn.lock +++ b/yarn.lock @@ -944,11 +944,6 @@ dependencies: "@types/node" "*" -"@types/cookie@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" - integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== - "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -980,14 +975,6 @@ dependencies: "@types/unist" "*" -"@types/hoist-non-react-statics@^3.3.5": - version "3.3.5" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" - integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1980,16 +1967,23 @@ convert-source-map@^1.5.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== -cookie@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" - integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookie@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== +cookies-next@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/cookies-next/-/cookies-next-5.0.2.tgz#441430194934b73945691c5b3a5e251833628c5c" + integrity sha512-Ft5yXMbN6wMfgLiL5TMyPnxsjkW6UEjZw0YMoDMiF3F6iYdFPjiJEMugx/sivUr8G+0xPG80lBYqI2b+VquSuw== + dependencies: + cookie "^1.0.1" + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -3284,7 +3278,7 @@ hexoid@^1.0.0: resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -5161,15 +5155,6 @@ react-confirm-alert@^3.0.6: resolved "https://registry.yarnpkg.com/react-confirm-alert/-/react-confirm-alert-3.0.6.tgz#f7552ec839c1e189370cef02ffed3035e7ca2e22" integrity sha512-rplP6Ed9ZSNd0KFV5BUzk4EPQ77BxsrayllBXGFuA8xPXc7sbBjgU5KUrNpl7aWFmP7mXRlVXfuy1IT5DbffYw== -react-cookie@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-7.2.2.tgz#a7559e552ea9cca39a4b3686723a5acf504b8f84" - integrity sha512-e+hi6axHcw9VODoeVu8WyMWyoosa1pzpyjfvrLdF7CexfU+WSGZdDuRfHa4RJgTpfv3ZjdIpHE14HpYBieHFhg== - dependencies: - "@types/hoist-non-react-statics" "^3.3.5" - hoist-non-react-statics "^3.3.2" - universal-cookie "^7.0.0" - react-dom@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -6244,14 +6229,6 @@ unist-util-visit@^5.0.0: unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -universal-cookie@^7.0.0: - version "7.2.2" - resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-7.2.2.tgz#93ae9ec55baab89b24300473543170bb8112773c" - integrity sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ== - dependencies: - "@types/cookie" "^0.6.0" - cookie "^0.7.2" - universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"