Added Redux + general changes
This commit is contained in:
parent
ba30f13e06
commit
e8d5b89999
22
jest.config.js
Normal file
22
jest.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
const nextJest = require('next/jest')
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
})
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const customJestConfig = {
|
||||
// Add more setup options before each test is run
|
||||
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
|
||||
moduleDirectories: ['node_modules', '<rootDir>/'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
moduleNameMapper: {
|
||||
'^/components/(.*)$': '<rootDir>/common/components/$1',
|
||||
'^/services/(.*)$': '<rootDir>/common/services/$1',
|
||||
},
|
||||
}
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig)
|
||||
@ -2,7 +2,8 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"components/*": ["./common/components/*"]
|
||||
"components/*": ["./common/components/*"],
|
||||
"services/*": ["./common/services/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,12 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
i18n: {
|
||||
// These are all the locales you want to support in
|
||||
// your application
|
||||
locales: ['en-US', 'fa-IR'],
|
||||
// This is the default locale you want to be used when visiting
|
||||
// a non-locale prefixed path e.g. `/hello`
|
||||
defaultLocale: 'fa-IR',
|
||||
localeDetection: false,
|
||||
},
|
||||
}
|
||||
|
||||
6770
package-lock.json
generated
6770
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -3,19 +3,27 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build": "next build && next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"test": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.5.9",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"graphql": "^16.3.0",
|
||||
"next": "12.0.10",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "^7.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "8.8.0",
|
||||
"eslint-config-next": "12.0.10",
|
||||
"jest": "^27.5.1",
|
||||
"postcss": "^8.4.6",
|
||||
"tailwindcss": "^3.0.18"
|
||||
}
|
||||
|
||||
BIN
public/fonts/IRANSansWeb.eot
Normal file
BIN
public/fonts/IRANSansWeb.eot
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb.ttf
Normal file
BIN
public/fonts/IRANSansWeb.ttf
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb.woff
Normal file
BIN
public/fonts/IRANSansWeb.woff
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb.woff2
Normal file
BIN
public/fonts/IRANSansWeb.woff2
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Bold.eot
Normal file
BIN
public/fonts/IRANSansWeb_Bold.eot
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Bold.ttf
Normal file
BIN
public/fonts/IRANSansWeb_Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Bold.woff
Normal file
BIN
public/fonts/IRANSansWeb_Bold.woff
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Bold.woff2
Normal file
BIN
public/fonts/IRANSansWeb_Bold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Medium.eot
Normal file
BIN
public/fonts/IRANSansWeb_Medium.eot
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Medium.ttf
Normal file
BIN
public/fonts/IRANSansWeb_Medium.ttf
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Medium.woff
Normal file
BIN
public/fonts/IRANSansWeb_Medium.woff
Normal file
Binary file not shown.
BIN
public/fonts/IRANSansWeb_Medium.woff2
Normal file
BIN
public/fonts/IRANSansWeb_Medium.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Roboto-400.woff2
Normal file
BIN
public/fonts/Roboto-400.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Roboto-500.woff2
Normal file
BIN
public/fonts/Roboto-500.woff2
Normal file
Binary file not shown.
@ -2,9 +2,17 @@ import React from "react"
|
||||
import NextImage from 'next/image'
|
||||
|
||||
const Image = ({caption, ...props}) => {
|
||||
|
||||
const cfLoader = ({ src, width, quality }) => {
|
||||
return `https://flierland.ca/cdn-cgi/imagedelivery/NB6v-3jb2osnFlFLXuQNUA/${props.src}?w=${props.width}&q=${props.quality || 75}`
|
||||
}
|
||||
|
||||
return (
|
||||
<figure className="relative flex justify-center">
|
||||
<NextImage {...props} />
|
||||
<NextImage
|
||||
{...props}
|
||||
loader={cfLoader}
|
||||
/>
|
||||
{caption && <figcaption>{caption}</figcaption>}
|
||||
</figure>
|
||||
)
|
||||
|
||||
11
src/common/data/apollo-client.js
Normal file
11
src/common/data/apollo-client.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { ApolloClient, InMemoryCache } from "@apollo/client";
|
||||
|
||||
const client = new ApolloClient({
|
||||
uri: 'http://localhost:8055/graphql',
|
||||
cache: new InMemoryCache()
|
||||
// headers: {
|
||||
// authorization: 'Bearer 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',
|
||||
// }
|
||||
});
|
||||
|
||||
export default client;
|
||||
19
src/common/redux/slices/global.js
Normal file
19
src/common/redux/slices/global.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const initialState = {
|
||||
value: {},
|
||||
}
|
||||
|
||||
export const globalSlice = createSlice({
|
||||
name: 'global',
|
||||
initialState,
|
||||
reducers: {
|
||||
setGlobalData: (state, action) => {
|
||||
state.value = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setGlobalData } = globalSlice.actions
|
||||
|
||||
export default globalSlice.reducer
|
||||
19
src/common/redux/slices/locale.js
Normal file
19
src/common/redux/slices/locale.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const initialState = {
|
||||
value: 'fa-IR',
|
||||
}
|
||||
|
||||
export const localeSlice = createSlice({
|
||||
name: 'locale',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLocale: (state, action) => {
|
||||
state.value = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setLocale } = localeSlice.actions
|
||||
|
||||
export default localeSlice.reducer
|
||||
8
src/common/redux/store/store.js
Normal file
8
src/common/redux/store/store.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import globalReducer from '../slices/global'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
global: globalReducer,
|
||||
},
|
||||
})
|
||||
17
src/common/services/translations.js
Normal file
17
src/common/services/translations.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const Translate = (token) => {
|
||||
|
||||
const router = useRouter();
|
||||
const locale = router.locale;
|
||||
const data = useSelector((state) => state.global.value);
|
||||
|
||||
const translation = data.dictionary
|
||||
.filter(x => x.token === token)
|
||||
.map(x => x.translations)[0]
|
||||
.filter(x => x.languages_code.code === locale)
|
||||
.map(x => x.translation)[0]
|
||||
return translation;
|
||||
}
|
||||
export default Translate
|
||||
3
src/common/services/translations.test.js
Normal file
3
src/common/services/translations.test.js
Normal file
@ -0,0 +1,3 @@
|
||||
it("Test works correctly!", () => {
|
||||
expect(2+2).toBe(4);
|
||||
});
|
||||
@ -2,43 +2,40 @@ import React, { useState, useEffect } from "react"
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from "components/link/link"
|
||||
import Image from "components/image/image"
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import MainMenu from './main-menu'
|
||||
|
||||
const Header = () => {
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const data = useSelector((state) => state.global.value);
|
||||
const router = useRouter();
|
||||
|
||||
const toggleMenu = () => {
|
||||
menuOpen ? setMenuOpen(false) : setMenuOpen(true);
|
||||
};
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
setMenuOpen(false);
|
||||
console.log('changed!');
|
||||
console.log('Route has changed!');
|
||||
}
|
||||
router.events.on('routeChangeComplete', handleRouteChange)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', handleRouteChange)
|
||||
}
|
||||
}, [])
|
||||
})
|
||||
|
||||
|
||||
const toggleMenu = () => {
|
||||
setMenuOpen(!menuOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="relative flex justify-between items-center py-1 px-1 bg-secondary shadow">
|
||||
<Link href="/"><Image src="/logo.png" width={180} height={64}/></Link>
|
||||
<div className="pr-2 cursor-pointer" onClick={toggleMenu}>
|
||||
<span className="block w-8 h-[3px] rounded bg-white mb-2"></span>
|
||||
<span className="block w-8 h-[3px] rounded bg-white mb-2"></span>
|
||||
<span className="block w-8 h-[3px] rounded bg-white"></span>
|
||||
</div>
|
||||
<nav className={`${menuOpen ? "block" : "hidden"} w-full absolute top-full left-0 bg-white shadow-sm`}>
|
||||
<div className="grid gap-1 p-1 grid-cols-1 px-3 py-3 border-b border-gray-200 border-solid border-t">
|
||||
<Link className="link select-none" href="/">Home</Link>
|
||||
<Link className="link select-none" href="/components">Components</Link>
|
||||
<Link className="link select-none" href="/about">About</Link>
|
||||
</div>
|
||||
</nav>
|
||||
<Link href="/"><Image src="08490987-279d-477f-5eab-58799e79cc00/public" alt="flierland logo" width={180} height={64}/></Link>
|
||||
<MainMenu data={data} isOpen={menuOpen}/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
133
src/common/templates/general/header/main-menu.js
Normal file
133
src/common/templates/general/header/main-menu.js
Normal file
@ -0,0 +1,133 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import translate from "services/translations"
|
||||
import Link from "components/link/link"
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const MainMenu = ({ data, isOpen }) => {
|
||||
|
||||
const router = useRouter();
|
||||
const [subActive, setSubActive] = useState([]);
|
||||
|
||||
const changeLocal = (locale) => {
|
||||
router.push('/', '/', { locale: locale })
|
||||
}
|
||||
|
||||
const openSub = (id) => {
|
||||
const menuStats = subActive;
|
||||
const current = menuStats.findIndex(x => x.id === id);
|
||||
if (current === -1) {
|
||||
menuStats.push({ id: id, isActive: true });
|
||||
setSubActive([...menuStats]);
|
||||
} else {
|
||||
menuStats[current].isActive = !menuStats[current].isActive;
|
||||
setSubActive([...menuStats]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
!isOpen && setSubActive([]);
|
||||
}, [isOpen])
|
||||
|
||||
// checks if this submenu is active now
|
||||
const matchId = (id) => {
|
||||
const isMatch = subActive.find(x => x.id === id) && subActive.find(x => x.id === id).isActive;
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
const MenuItem = ({ title, href, isSub, className }) => {
|
||||
return <Link className={`block link select-none ${isSub ? 'text-sm py-2' : 'text-base py-3'} w-full ${className}`} href={href}>{title}</Link>
|
||||
}
|
||||
|
||||
const MenuBuilder = ({ menu, title, href }) => {
|
||||
return (
|
||||
<li className={`block w-full px-3 ${matchId(title) ? 'border-r-2 border-solid border-r-primary' : ''} border-t border-b border-t-white border-b-border-gray-bottom first:border-t-0 last:border-b-0`}>
|
||||
<div className="flex justify-between w-full flex-row items-center">
|
||||
{href ?
|
||||
// lvl one
|
||||
<MenuItem className={`${matchId(title) ? 'text-primary' : ''}`} title={translate(title)} href={href} />
|
||||
:
|
||||
// lvl one
|
||||
<span className={`block w-full link select-none leading-4 py-3 ${matchId(title) ? 'text-primary' : ''}`}>{translate(title)}</span>
|
||||
}
|
||||
{menu &&
|
||||
<span
|
||||
className={`
|
||||
${matchId(title) ? '-rotate-90 text-primary' : ''}
|
||||
w-8 text-center text-2xl font-bold cursor-pointer hover:text-primary
|
||||
`}
|
||||
onClick={() => openSub(title)}
|
||||
>›
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
{menu &&
|
||||
<ul className={`${matchId(title) ? 'block' : 'hidden'} px-3 mb-2`} >
|
||||
{menu !== null && menu.map(item => (
|
||||
item.has_children ?
|
||||
// lvl two
|
||||
<li className="border-t border-b border-t-white border-b-border-gray-bottom first:border-t-0 last:border-b-0" key={item.id}>
|
||||
<div className="flex justify-between w-full flex-row items-center">
|
||||
<MenuItem className={`${matchId(item.id) ? 'text-primary' : ''}`} title={item.name} href={"/"} isSub={true}/>
|
||||
<span
|
||||
className={`
|
||||
${matchId(item.id) ? '-rotate-90 text-primary' : ''}
|
||||
w-8 text-center text-2xl font-bold cursor-pointer hover:text-primary
|
||||
`}
|
||||
onClick={() => openSub(item.id)}
|
||||
>›
|
||||
</span>
|
||||
</div>
|
||||
<ul className={`${matchId(item.id) ? 'block' : 'hidden'} px-3 pb-2`}>
|
||||
{menu.filter(x => x.parent && x.parent.length > 0 && x.parent[0].related_magazine_categories_id.name === item.name)
|
||||
.map(x => (
|
||||
// lvl three
|
||||
<li className ="border-t border-b border-t-white border-b-border-gray-bottom first:border-t-0 last:border-b-0 hover:bg-white" key={x.id}>
|
||||
<div className="flex justify-between w-full flex-row items-center">
|
||||
<span
|
||||
className={`w-8 text-center text-xl`}
|
||||
>›
|
||||
</span>
|
||||
<MenuItem title={x.name} href="/" isSub={true}/>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
:
|
||||
item.parent && item.parent.length < 1 ?
|
||||
// lvl two
|
||||
<li className="border-t border-b border-t-white border-b-border-gray-bottom first:border-t-0 last:border-b-0 hover:bg-white" key={item.id}>
|
||||
<div className="flex justify-between w-full flex-row items-center">
|
||||
<span
|
||||
className={`w-8 text-center text-xl`}
|
||||
>›
|
||||
</span>
|
||||
<MenuItem title={item.name} href="/" isSub={true} />
|
||||
</div>
|
||||
</li> : ''
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={`${isOpen ? "block" : "hidden"} w-full absolute top-full left-0 bg-light-gray shadow`}>
|
||||
{/* Main menu */}
|
||||
<ul className="grid gap-0 p-1 grid-cols-1 px-0 py-3 border-b border-border-gray-bottom border-solid">
|
||||
<MenuBuilder title="main-menu-home-title" href={"/"}/>
|
||||
<MenuBuilder title="main-menu-billboard-title" menu={data.billboard_categories}/>
|
||||
<MenuBuilder title="main-menu-magazine-title" menu={data.magazine_categories} />
|
||||
<MenuBuilder title="main-menu-contact-title" href={"/contact"}/>
|
||||
</ul>
|
||||
{/* Locale selection */}
|
||||
<div className="grid grid-cols-2 border-t border-solid border-white p-2">
|
||||
<button className="text-center" onClick={() => changeLocal('fa-IR')}>fa-IR</button>
|
||||
<button className="text-center" onClick={() => changeLocal('en-US')}>en-US</button>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
export default MainMenu
|
||||
@ -1,10 +1,56 @@
|
||||
import React from "react"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import Header from "../header/header"
|
||||
import Footer from "../footer/footer"
|
||||
import { useQuery, gql } from "@apollo/client";
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { setGlobalData } from '../../../redux/slices/global'
|
||||
|
||||
const Layout = ({children}) => {
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const getGlobalData = gql`
|
||||
query GetTranslations {
|
||||
dictionary {
|
||||
token
|
||||
translations {
|
||||
languages_code {
|
||||
code
|
||||
}
|
||||
translation
|
||||
}
|
||||
}
|
||||
billboard_categories {
|
||||
name
|
||||
id
|
||||
has_children
|
||||
parent {
|
||||
related_billboard_categories_id {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
magazine_categories {
|
||||
name
|
||||
id
|
||||
has_children
|
||||
parent {
|
||||
related_magazine_categories_id {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(getGlobalData);
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p>{error.message} :(</p>;
|
||||
dispatch(setGlobalData(data))
|
||||
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="" style={{'direction': 'rtl'}}>
|
||||
<Header />
|
||||
<main>
|
||||
{children}
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import './styles/globals.css'
|
||||
import Layout from '../common/templates/general/layout/layout'
|
||||
import { store } from '../common/redux/store/store'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, gql } from "@apollo/client";
|
||||
import client from "../common/data/apollo-client";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
<Provider store={store}>
|
||||
<ApolloProvider client={client}>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</ApolloProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
export default MyApp
|
||||
@ -1,7 +1,7 @@
|
||||
import Image from 'next/image'
|
||||
import translate from "services/translations"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<h1 className="">Deltora react components</h1>
|
||||
<h1 className="">{translate("home-page-main-title")}</h1>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,47 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* :root {
|
||||
|
||||
--primary: #e12828;
|
||||
--secondary: #2A2E3C;
|
||||
--sup-light: #E6E6EA;
|
||||
--sup-dark: #fed766;
|
||||
--light-bg: #f8f8f8;
|
||||
--icon-color: #757575;
|
||||
--title-color: #2b2b2b;
|
||||
--text-color: #464040;
|
||||
|
||||
--main-font: IRANSans, sans-serif;
|
||||
--fs: 16px;
|
||||
--fw-normal: 400;
|
||||
--fw-semi-bold: 500;
|
||||
|
||||
--gi-side: 92px;
|
||||
--gi-double: 184px;
|
||||
--li-side: calc((100vw - 1400px) / 2);
|
||||
--li-double: calc(100vw - 1400px);
|
||||
|
||||
--scale: scale(95);
|
||||
} */
|
||||
|
||||
@font-face {
|
||||
font-family: IRANSans;
|
||||
src: local('IRANSans'), url(/fonts/IRANSansWeb.woff2) format("woff2"),
|
||||
url(/fonts/IRANSansWeb.woff) format("woff"),
|
||||
url(/fonts/IRANSansWeb.ttf) format("truetype"),
|
||||
url(/fonts/IRANSansWeb.eot),
|
||||
url(/fonts/IRANSansWeb.eot?#iefix) format("embedded-opentype");
|
||||
unicode-range: 0020-007F, 00A0-00BB, 0600-067F;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply font-main;
|
||||
}
|
||||
h1 {
|
||||
@apply text-2xl font-bold;
|
||||
}
|
||||
|
||||
@ -6,9 +6,14 @@ module.exports = {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
'main': ['IRANSans', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
'primary': '#e12828',
|
||||
'secondary': '#2A2E3C',
|
||||
'light-gray': '#f5f7fa',
|
||||
'border-gray-bottom': '#e0e7f0',
|
||||
'sup-light': '#E6E6EA',
|
||||
'sup-dark': '#fed766',
|
||||
'light-bg': '#f8f8f8',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user