diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 99ec8dd..4f76e4a 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -114,6 +114,7 @@ module.exports = {
'**/*.js',
'**/*.cjs',
'**/setupTests.ts',
+ '**/testUtils.tsx'
],
rules: {
'import/no-extraneous-dependencies': [
diff --git a/additional.d.ts b/additional.d.ts
index 60260a3..2083be8 100644
--- a/additional.d.ts
+++ b/additional.d.ts
@@ -1 +1,11 @@
+import 'styled-components';
+import type {theme} from '@/src/style';
+
declare module '*.module.css';
+
+type CustomTheme = typeof theme;
+
+declare module 'styled-components' {
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */
+ export interface DefaultTheme extends CustomTheme {}
+}
diff --git a/app/index.css b/app/index.css
deleted file mode 100644
index 43ac7ad..0000000
--- a/app/index.css
+++ /dev/null
@@ -1,8 +0,0 @@
-body {
- font-family: system-ui;
- margin: 0;
-}
-
-main {
- padding: 36px;
-}
diff --git a/app/layout.tsx b/app/layout.tsx
index 65a5c92..46d304b 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,7 +1,7 @@
import type {ReactNode} from 'react';
import {StoreProvider} from '@/src/state/StoreProvider';
-import './index.css';
+import {StyledComponentsRegistry, ThemeProvider} from '@/src/style';
type Props = {
readonly children: ReactNode;
@@ -10,9 +10,13 @@ type Props = {
export default function RootLayout({children}: Props) {
return (
-
- {children}
-
+
+
+
+ {children}
+
+
+
);
}
diff --git a/next.config.js b/next.config.js
index 65be271..e0dab2b 100644
--- a/next.config.js
+++ b/next.config.js
@@ -9,7 +9,10 @@ module.exports = withBundleAnalyzer({
reactStrictMode: true,
swcMinify: true,
distDir: 'build',
- output: process.env.PAGES_BUILD === 'true' ? 'export' : undefined,
cleanDistDir: true,
+ output: process.env.PAGES_BUILD === 'true' ? 'export' : undefined,
basePath: process.env.PAGES_BUILD === 'true' ? '/ts-redux-next' : undefined,
+ compiler: {
+ styledComponents: true,
+ },
});
diff --git a/package.json b/package.json
index d67e744..f53f985 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,8 @@
"start": "next start",
"lint:code": "eslint src/ --ext .js,.jsx,.ts,.tsx",
"fix:code": "run-s 'lint:code --fix'",
- "lint:style": "stylelint 'src/**/*.css'",
- "fix:style": "stylelint 'src/**/*.css' --fix",
+ "lint:style": "stylelint 'src/**/*.{ts,tsx}'",
+ "fix:style": "stylelint 'src/**/*.{ts,tsx}' --fix",
"lint:tsc": "tsc --pretty --noEmit",
"prepare": "is-ci || husky install",
"test": "jest",
@@ -27,6 +27,7 @@
"react-dom": "18.3.1",
"react-redux": "9.1.2",
"redux": "5.0.1",
+ "styled-components": "^6.1.11",
"uniqid": "5.4.0"
},
"devDependencies": {
@@ -59,13 +60,12 @@
"npm-run-all2": "6.1.2",
"postcss": "8.4.38",
"postcss-preset-env": "9.5.13",
+ "postcss-styled-syntax": "0.6.4",
"prettier": "3.2.5",
"redux-mock-store": "1.5.4",
"stylelint": "16.5.0",
- "stylelint-config-prettier": "9.0.5",
"stylelint-config-standard": "36.0.0",
"stylelint-order": "6.0.4",
- "stylelint-prettier": "5.0.0",
"typescript": "5.4.5"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b459422..bb5403d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
redux:
specifier: 5.0.1
version: 5.0.1
+ styled-components:
+ specifier: ^6.1.11
+ version: 6.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
uniqid:
specifier: 5.4.0
version: 5.4.0
@@ -120,6 +123,9 @@ importers:
postcss-preset-env:
specifier: 9.5.13
version: 9.5.13(postcss@8.4.38)
+ postcss-styled-syntax:
+ specifier: 0.6.4
+ version: 0.6.4(postcss@8.4.38)
prettier:
specifier: 3.2.5
version: 3.2.5
@@ -129,18 +135,12 @@ importers:
stylelint:
specifier: 16.5.0
version: 16.5.0(typescript@5.4.5)
- stylelint-config-prettier:
- specifier: 9.0.5
- version: 9.0.5(stylelint@16.5.0(typescript@5.4.5))
stylelint-config-standard:
specifier: 36.0.0
version: 36.0.0(stylelint@16.5.0(typescript@5.4.5))
stylelint-order:
specifier: 6.0.4
version: 6.0.4(stylelint@16.5.0(typescript@5.4.5))
- stylelint-prettier:
- specifier: 5.0.0
- version: 5.0.0(prettier@3.2.5)(stylelint@16.5.0(typescript@5.4.5))
typescript:
specifier: 5.4.5
version: 5.4.5
@@ -1199,6 +1199,15 @@ packages:
'@dual-bundle/import-meta-resolve@4.1.0':
resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==}
+ '@emotion/is-prop-valid@1.2.2':
+ resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==}
+
+ '@emotion/memoize@0.8.1':
+ resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==}
+
+ '@emotion/unitless@0.8.1':
+ resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
+
'@eslint-community/eslint-utils@4.4.0':
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1588,6 +1597,9 @@ packages:
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
+ '@types/stylis@4.2.5':
+ resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==}
+
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
@@ -2002,6 +2014,9 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
+ camelize@1.0.1:
+ resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
+
caniuse-lite@1.0.30001617:
resolution: {integrity: sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==}
@@ -2161,6 +2176,10 @@ packages:
peerDependencies:
postcss: ^8.4
+ css-color-keywords@1.0.0:
+ resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
+ engines: {node: '>=4'}
+
css-functions-list@3.2.2:
resolution: {integrity: sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==}
engines: {node: '>=12 || >=16'}
@@ -2177,6 +2196,9 @@ packages:
peerDependencies:
postcss: ^8.4
+ css-to-react-native@3.2.0:
+ resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==}
+
css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -2205,6 +2227,9 @@ packages:
csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -3988,6 +4013,12 @@ packages:
peerDependencies:
postcss: ^8.4.20
+ postcss-styled-syntax@0.6.4:
+ resolution: {integrity: sha512-uWiLn+9rKgIghUYmTHvXMR6MnyPULMe9Gv3bV537Fg4FH6CA6cn21WMjKss2Qb98LUhT847tKfnRGG3FhSOgUQ==}
+ engines: {node: '>=14.17'}
+ peerDependencies:
+ postcss: ^8.4.21
+
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -4280,6 +4311,9 @@ packages:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
engines: {node: '>= 0.4'}
+ shallowequal@1.1.0:
+ resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -4445,6 +4479,13 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ styled-components@6.1.11:
+ resolution: {integrity: sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ react: '>= 16.8.0'
+ react-dom: '>= 16.8.0'
+
styled-jsx@5.1.1:
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
@@ -4458,13 +4499,6 @@ packages:
babel-plugin-macros:
optional: true
- stylelint-config-prettier@9.0.5:
- resolution: {integrity: sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==}
- engines: {node: '>= 12'}
- hasBin: true
- peerDependencies:
- stylelint: '>= 11.x < 15'
-
stylelint-config-recommended@14.0.0:
resolution: {integrity: sha512-jSkx290CglS8StmrLp2TxAppIajzIBZKYm3IxT89Kg6fGlxbPiTiyH9PS5YUuVAFwaJLl1ikiXX0QWjI0jmgZQ==}
engines: {node: '>=18.12.0'}
@@ -4482,18 +4516,14 @@ packages:
peerDependencies:
stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1
- stylelint-prettier@5.0.0:
- resolution: {integrity: sha512-RHfSlRJIsaVg5Br94gZVdWlz/rBTyQzZflNE6dXvSxt/GthWMY3gEHsWZEBaVGg7GM+XrtVSp4RznFlB7i0oyw==}
- engines: {node: '>=18.12.0'}
- peerDependencies:
- prettier: '>=3.0.0'
- stylelint: '>=16.0.0'
-
stylelint@16.5.0:
resolution: {integrity: sha512-IlCBtVrG+qTy3v+tZTk50W8BIomjY/RUuzdrDqdnlCYwVuzXtPbiGfxYqtyYAyOMcb+195zRsuHn6tgfPmFfbw==}
engines: {node: '>=18.12.0'}
hasBin: true
+ stylis@4.3.2:
+ resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==}
+
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@@ -6189,6 +6219,14 @@ snapshots:
'@dual-bundle/import-meta-resolve@4.1.0': {}
+ '@emotion/is-prop-valid@1.2.2':
+ dependencies:
+ '@emotion/memoize': 0.8.1
+
+ '@emotion/memoize@0.8.1': {}
+
+ '@emotion/unitless@0.8.1': {}
+
'@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)':
dependencies:
eslint: 8.57.0
@@ -6684,6 +6722,8 @@ snapshots:
'@types/stack-utils@2.0.3': {}
+ '@types/stylis@4.2.5': {}
+
'@types/tough-cookie@4.0.5': {}
'@types/uniqid@5.3.4': {}
@@ -7239,6 +7279,8 @@ snapshots:
camelcase@6.3.0: {}
+ camelize@1.0.1: {}
+
caniuse-lite@1.0.30001617: {}
chalk@2.4.2:
@@ -7401,6 +7443,8 @@ snapshots:
postcss: 8.4.38
postcss-selector-parser: 6.0.13
+ css-color-keywords@1.0.0: {}
+
css-functions-list@3.2.2: {}
css-has-pseudo@6.0.5(postcss@8.4.38):
@@ -7414,6 +7458,12 @@ snapshots:
dependencies:
postcss: 8.4.38
+ css-to-react-native@3.2.0:
+ dependencies:
+ camelize: 1.0.1
+ css-color-keywords: 1.0.0
+ postcss-value-parser: 4.2.0
+
css-tree@2.3.1:
dependencies:
mdn-data: 2.0.30
@@ -7435,6 +7485,8 @@ snapshots:
csstype@3.1.2: {}
+ csstype@3.1.3: {}
+
damerau-levenshtein@1.0.8: {}
data-urls@3.0.2:
@@ -9763,6 +9815,11 @@ snapshots:
dependencies:
postcss: 8.4.38
+ postcss-styled-syntax@0.6.4(postcss@8.4.38):
+ dependencies:
+ postcss: 8.4.38
+ typescript: 5.4.5
+
postcss-value-parser@4.2.0: {}
postcss@8.4.31:
@@ -10077,6 +10134,8 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
+ shallowequal@1.1.0: {}
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -10258,6 +10317,20 @@ snapshots:
strip-json-comments@3.1.1: {}
+ styled-components@6.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@emotion/is-prop-valid': 1.2.2
+ '@emotion/unitless': 0.8.1
+ '@types/stylis': 4.2.5
+ css-to-react-native: 3.2.0
+ csstype: 3.1.3
+ postcss: 8.4.38
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ shallowequal: 1.1.0
+ stylis: 4.3.2
+ tslib: 2.6.2
+
styled-jsx@5.1.1(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react@18.3.1):
dependencies:
client-only: 0.0.1
@@ -10266,10 +10339,6 @@ snapshots:
'@babel/core': 7.24.5
babel-plugin-macros: 3.1.0
- stylelint-config-prettier@9.0.5(stylelint@16.5.0(typescript@5.4.5)):
- dependencies:
- stylelint: 16.5.0(typescript@5.4.5)
-
stylelint-config-recommended@14.0.0(stylelint@16.5.0(typescript@5.4.5)):
dependencies:
stylelint: 16.5.0(typescript@5.4.5)
@@ -10285,12 +10354,6 @@ snapshots:
postcss-sorting: 8.0.2(postcss@8.4.38)
stylelint: 16.5.0(typescript@5.4.5)
- stylelint-prettier@5.0.0(prettier@3.2.5)(stylelint@16.5.0(typescript@5.4.5)):
- dependencies:
- prettier: 3.2.5
- prettier-linter-helpers: 1.0.0
- stylelint: 16.5.0(typescript@5.4.5)
-
stylelint@16.5.0(typescript@5.4.5):
dependencies:
'@csstools/css-parser-algorithms': 2.6.3(@csstools/css-tokenizer@2.3.1)
@@ -10336,6 +10399,8 @@ snapshots:
- supports-color
- typescript
+ stylis@4.3.2: {}
+
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
diff --git a/src/components/Counter/Counter.spec.tsx b/src/components/Counter/Counter.spec.tsx
index ddd5dc5..8141142 100644
--- a/src/components/Counter/Counter.spec.tsx
+++ b/src/components/Counter/Counter.spec.tsx
@@ -1,9 +1,10 @@
import React from 'react';
import {Provider} from 'react-redux';
-import {render, fireEvent} from '@testing-library/react';
+import {fireEvent} from '@testing-library/react';
import configureStore from 'redux-mock-store';
import {Actions} from '@/src/features/counter/actionTypes';
+import {render} from '@/src/testUtils';
import Counter from './Counter';
diff --git a/src/components/Random/Random.module.css b/src/components/Counter/Counter.style.ts
similarity index 58%
rename from src/components/Random/Random.module.css
rename to src/components/Counter/Counter.style.ts
index a806b74..bc3aa92 100644
--- a/src/components/Random/Random.module.css
+++ b/src/components/Counter/Counter.style.ts
@@ -1,19 +1,21 @@
-.random {
+import {styled} from 'styled-components';
+
+export const Wrapper = styled.div`
border: 1px solid lightgray;
margin-bottom: 24px;
padding: 24px;
+ text-align: center;
width: 240px;
-}
+`;
-.header {
+export const Header = styled.h2`
font-size: 24px;
font-weight: normal;
margin: 0 0 12px;
- text-align: center;
-}
+`;
-.button {
- background: lightseagreen;
+export const Button = styled.button`
+ background: ${props => props.theme.colors.brand};
border: none;
border-radius: 5px;
color: white;
@@ -23,15 +25,10 @@
margin: 0 auto 24px;
padding: 12px 24px;
text-shadow: 1px 1px 1px rgb(0 0 0 / 50%);
-}
-
-.button:disabled {
- background: lightgray;
- cursor: not-allowed;
-}
-.button:active:not(:disabled) {
- left: 1px;
- position: relative;
- top: 1px;
-}
+ &:active {
+ left: 1px;
+ position: relative;
+ top: 1px;
+ }
+`;
diff --git a/src/components/Counter/Counter.tsx b/src/components/Counter/Counter.tsx
index 3a30479..6e13415 100644
--- a/src/components/Counter/Counter.tsx
+++ b/src/components/Counter/Counter.tsx
@@ -5,7 +5,7 @@ import React from 'react';
import {useCountValue, useIncrementCounter} from '@/src/features/counter';
-import classes from './Counter.module.css';
+import {Wrapper, Header, Button} from './Counter.style';
const Counter: FC = () => {
/**
@@ -19,15 +19,13 @@ const Counter: FC = () => {
const incrementCounter = useIncrementCounter();
return (
-
-
Sync counter
-
- Increment by one
-
+
+
+ Increment by one
Total value: {count}
-
+
);
};
diff --git a/src/components/Counter/__snapshots__/Counter.spec.tsx.snap b/src/components/Counter/__snapshots__/Counter.spec.tsx.snap
index eb9dbb3..5c38308 100644
--- a/src/components/Counter/__snapshots__/Counter.spec.tsx.snap
+++ b/src/components/Counter/__snapshots__/Counter.spec.tsx.snap
@@ -3,16 +3,15 @@
exports[`components > Counter renders without crashing 1`] = `
Increment by one
diff --git a/src/components/Random/Random.spec.tsx b/src/components/Random/Random.spec.tsx
index 2370019..8b608ec 100644
--- a/src/components/Random/Random.spec.tsx
+++ b/src/components/Random/Random.spec.tsx
@@ -2,7 +2,6 @@ import React from 'react';
import {Provider} from 'react-redux';
import type {Store, Action} from 'redux';
import {
- render,
fireEvent,
waitFor,
screen,
@@ -10,6 +9,7 @@ import {
} from '@testing-library/react';
import configureStore from 'redux-mock-store';
+import {render} from '@/src/testUtils';
import {GET_RANDOM_NUMBER} from '@/src/features/random/actionTypes';
import {makeStore} from '@/src/state/store';
import {promiseResolverMiddleware} from '@/src/state/promiseResolverMiddleware';
diff --git a/src/components/Counter/Counter.module.css b/src/components/Random/Random.style.ts
similarity index 50%
rename from src/components/Counter/Counter.module.css
rename to src/components/Random/Random.style.ts
index dc20593..f2d369f 100644
--- a/src/components/Counter/Counter.module.css
+++ b/src/components/Random/Random.style.ts
@@ -1,19 +1,21 @@
-.counter {
+import {styled} from 'styled-components';
+
+export const Wrapper = styled.div`
border: 1px solid lightgray;
margin-bottom: 24px;
padding: 24px;
text-align: center;
width: 240px;
-}
+`;
-.header {
+export const Header = styled.h2`
font-size: 24px;
font-weight: normal;
margin: 0 0 12px;
-}
+`;
-.button {
- background: lightseagreen;
+export const Button = styled.button`
+ background: ${props => props.theme.colors.brand};
border: none;
border-radius: 5px;
color: white;
@@ -23,10 +25,15 @@
margin: 0 auto 24px;
padding: 12px 24px;
text-shadow: 1px 1px 1px rgb(0 0 0 / 50%);
-}
-.button:active {
- left: 1px;
- position: relative;
- top: 1px;
-}
+ &:disabled {
+ background: lightgray;
+ cursor: not-allowed;
+ }
+
+ &:active:not(:disabled) {
+ left: 1px;
+ position: relative;
+ top: 1px;
+ }
+`;
diff --git a/src/components/Random/Random.tsx b/src/components/Random/Random.tsx
index 6408735..eed846a 100644
--- a/src/components/Random/Random.tsx
+++ b/src/components/Random/Random.tsx
@@ -8,7 +8,7 @@ import {
useLoadingState,
} from '@/src/features/random';
-import classes from './Random.module.css';
+import {Wrapper, Header, Button} from './Random.style';
const Random = () => {
/** Loading state of random.org request from Redux store */
@@ -24,15 +24,11 @@ const Random = () => {
const isPristine = !isLoading && !hasError && !isFulfilled;
return (
-
-
Async Random
-
+
+
+
Get random number
-
+
{isPristine &&
Click the button to get random number
}
{isLoading &&
Getting number
}
{isFulfilled && (
@@ -41,7 +37,7 @@ const Random = () => {
)}
{hasError &&
Ups...
}
-
+
);
};
diff --git a/src/components/Random/__snapshots__/Random.spec.tsx.snap b/src/components/Random/__snapshots__/Random.spec.tsx.snap
index aaa60fd..14fd946 100644
--- a/src/components/Random/__snapshots__/Random.spec.tsx.snap
+++ b/src/components/Random/__snapshots__/Random.spec.tsx.snap
@@ -3,15 +3,15 @@
exports[`components > Random handles rejected request 1`] = `
@@ -27,15 +27,15 @@ exports[`components > Random handles rejected request 1`] = `
exports[`components > Random handles rejected request 2`] = `
Get random number
@@ -50,15 +50,15 @@ exports[`components > Random handles rejected request 2`] = `
exports[`components > Random handles successful request 1`] = `
@@ -74,15 +74,15 @@ exports[`components > Random handles successful request 1`] = `
exports[`components > Random handles successful request 2`] = `
Get random number
@@ -100,15 +100,15 @@ exports[`components > Random handles successful request 2`] = `
exports[`components > Random renders different store states when isLoading === false && hasError === false && isFulfilled === false 1`] = `
Get random number
@@ -123,15 +123,15 @@ exports[`components > Random renders different store states when isLoading === f
exports[`components > Random renders different store states when isLoading === false && hasError === false && isFulfilled === true 1`] = `
Get random number
@@ -149,15 +149,15 @@ exports[`components > Random renders different store states when isLoading === f
exports[`components > Random renders different store states when isLoading === false && hasError === true && isFulfilled === false 1`] = `
Get random number
@@ -172,15 +172,15 @@ exports[`components > Random renders different store states when isLoading === f
exports[`components > Random renders different store states when isLoading === true && hasError === false && isFulfilled === false 1`] = `
diff --git a/src/layout/NavHeader/NavHeader.module.css b/src/layout/NavHeader/NavHeader.style.ts
similarity index 63%
rename from src/layout/NavHeader/NavHeader.module.css
rename to src/layout/NavHeader/NavHeader.style.ts
index b9b329b..ff9dea3 100644
--- a/src/layout/NavHeader/NavHeader.module.css
+++ b/src/layout/NavHeader/NavHeader.style.ts
@@ -1,14 +1,16 @@
-.navHeader {
+import {styled} from 'styled-components';
+
+export const Header = styled.header`
align-items: center;
- background: lightseagreen;
+ background: ${props => props.theme.colors.brand};
display: flex;
flex-direction: row;
justify-content: center;
padding: 12px 24px;
position: sticky;
-}
+`;
-.name {
+export const Name = styled.h1`
color: white;
cursor: default;
font-size: 24px;
@@ -16,18 +18,10 @@
margin: 0;
text-shadow: 1px 1px 1px rgb(0 0 0 / 66%);
user-select: none;
-}
+`;
-.navigation {
+export const Navigation = styled.div`
display: flex;
gap: 18px;
margin-left: auto;
-}
-
-.link {
- border-bottom: 2px solid white;
- color: white;
- font-size: 16px;
- font-weight: bold;
- text-decoration: none;
-}
+`;
diff --git a/src/layout/NavHeader/NavHeader.tsx b/src/layout/NavHeader/NavHeader.tsx
index c2141b1..c9b67fc 100644
--- a/src/layout/NavHeader/NavHeader.tsx
+++ b/src/layout/NavHeader/NavHeader.tsx
@@ -1,18 +1,20 @@
+'use client';
+
import React from 'react';
import type {FC} from 'react';
import {NavLink} from '@/src/layout/NavLink';
-import classes from './NavHeader.module.css';
+import {Header, Name, Navigation} from './NavHeader.style';
export const NavHeader: FC = () => {
return (
-
+
+
);
};
diff --git a/src/layout/NavLink/NavLink.module.css b/src/layout/NavLink/NavLink.module.css
deleted file mode 100644
index 31ed3c4..0000000
--- a/src/layout/NavLink/NavLink.module.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.navLink {
- color: white;
- font-size: 16px;
- font-weight: bold;
- text-decoration: none;
- text-shadow: 1px 1px 1px rgb(0 0 0 / 66%);
- user-select: none;
-}
-
-.active {
- border-bottom: 2px solid white;
- box-shadow: 0 1px 0 0 rgb(0 0 0 / 66%);
- cursor: default;
- pointer-events: none;
-}
diff --git a/src/layout/NavLink/NavLink.style.ts b/src/layout/NavLink/NavLink.style.ts
new file mode 100644
index 0000000..d5faedb
--- /dev/null
+++ b/src/layout/NavLink/NavLink.style.ts
@@ -0,0 +1,21 @@
+import {styled, css} from 'styled-components';
+import NextLink from 'next/link';
+import type {ComponentProps} from 'react';
+
+export const Link = styled(NextLink) & {active: boolean}>`
+ color: white;
+ font-size: 16px;
+ font-weight: bold;
+ text-decoration: none;
+ text-shadow: 1px 1px 1px rgb(0 0 0 / 66%);
+ user-select: none;
+
+ ${({active}) =>
+ active &&
+ css`
+ border-bottom: 2px solid white;
+ box-shadow: 0 1px 0 0 rgb(0 0 0 / 66%);
+ cursor: default;
+ pointer-events: none;
+ `};
+`;
diff --git a/src/layout/NavLink/NavLink.tsx b/src/layout/NavLink/NavLink.tsx
index 66a25f8..a0b2d6c 100644
--- a/src/layout/NavLink/NavLink.tsx
+++ b/src/layout/NavLink/NavLink.tsx
@@ -1,27 +1,20 @@
'use client';
import type {FC, ReactNode, ComponentProps} from 'react';
-import NextLink from 'next/link';
-import classNames from 'classnames';
+import type NextLink from 'next/link';
import {usePathname} from 'next/navigation';
-import classes from './NavLink.module.css';
+import {Link} from './NavLink.style';
export type Props = ComponentProps & {
children?: ReactNode;
};
-export const NavLink: FC = ({children, href, className}) => {
+export const NavLink: FC = ({children, href}) => {
const currentPath = usePathname();
return (
-
+
{children}
-
+
);
};
diff --git a/src/style/GlobalStyle.ts b/src/style/GlobalStyle.ts
new file mode 100644
index 0000000..18d12bf
--- /dev/null
+++ b/src/style/GlobalStyle.ts
@@ -0,0 +1,14 @@
+'use client';
+
+import {createGlobalStyle} from 'styled-components';
+
+export const GlobalStyle = createGlobalStyle`
+ body {
+ font-family: system-ui;
+ margin: 0;
+ }
+
+ main {
+ padding: 36px;
+ }
+`;
diff --git a/src/style/StyledComponentsRegistry.tsx b/src/style/StyledComponentsRegistry.tsx
new file mode 100644
index 0000000..05997b3
--- /dev/null
+++ b/src/style/StyledComponentsRegistry.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import type {FC, ReactNode} from 'react';
+import {useState} from 'react';
+import {useServerInsertedHTML} from 'next/navigation';
+import {ServerStyleSheet, StyleSheetManager} from 'styled-components';
+
+import {GlobalStyle} from './GlobalStyle';
+
+export const StyledComponentsRegistry: FC<{children: ReactNode}> = ({children}) => {
+ // Only create stylesheet once with lazy initial state
+ // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
+ const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
+
+ useServerInsertedHTML(() => {
+ const styles = styledComponentsStyleSheet.getStyleElement();
+ styledComponentsStyleSheet.instance.clearTag();
+ return <>{styles}>;
+ });
+
+ if (typeof window !== 'undefined') return <>{children}>;
+
+ return (
+
+
+ {children}
+
+ );
+};
diff --git a/src/style/ThemeProvider.tsx b/src/style/ThemeProvider.tsx
new file mode 100644
index 0000000..fb2ea77
--- /dev/null
+++ b/src/style/ThemeProvider.tsx
@@ -0,0 +1,10 @@
+'use client';
+
+import {ThemeProvider as Provider} from 'styled-components';
+import type {FC, ReactNode} from 'react';
+
+import {theme} from './theme';
+
+export const ThemeProvider: FC<{children: ReactNode}> = ({children}) => {
+ return {children} ;
+};
diff --git a/src/style/index.ts b/src/style/index.ts
new file mode 100644
index 0000000..7e8e812
--- /dev/null
+++ b/src/style/index.ts
@@ -0,0 +1,4 @@
+export {StyledComponentsRegistry} from './StyledComponentsRegistry';
+export {theme} from './theme';
+export {ThemeProvider} from './ThemeProvider';
+export {GlobalStyle} from './GlobalStyle';
diff --git a/src/style/theme.ts b/src/style/theme.ts
new file mode 100644
index 0000000..f80236c
--- /dev/null
+++ b/src/style/theme.ts
@@ -0,0 +1,5 @@
+export const theme = {
+ colors: {
+ brand: '#20B2AAFF',
+ },
+};
diff --git a/src/testUtils.tsx b/src/testUtils.tsx
new file mode 100644
index 0000000..61deca8
--- /dev/null
+++ b/src/testUtils.tsx
@@ -0,0 +1,21 @@
+import {render as renderVanilla} from '@testing-library/react';
+import type {ReactElement} from 'react';
+import {Fragment} from 'react';
+import type {RenderOptions, RenderResult} from '@testing-library/react';
+
+import {ThemeProvider} from '@/src/style';
+
+export const render = (
+ Component: ReactElement,
+ options?: RenderOptions
+): RenderResult => {
+ const ExtraWrapper = options?.wrapper ? options?.wrapper : Fragment;
+ return renderVanilla(Component, {
+ ...options,
+ wrapper: ({children}) => (
+
+ {children}
+
+ ),
+ });
+};
diff --git a/stylelint.config.js b/stylelint.config.js
index c859b1e..d74e8f1 100644
--- a/stylelint.config.js
+++ b/stylelint.config.js
@@ -1,22 +1,30 @@
module.exports = {
- extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
+ extends: ['stylelint-config-standard'],
+ customSyntax: 'postcss-styled-syntax',
+ plugins: ['stylelint-order'],
rules: {
- 'function-calc-no-unspaced-operator': true,
+ /**
+ * Enforce alphabetical order for properties and custom properties before standard
+ * @see https://github.com/hudochenkov/stylelint-order
+ */
'order/order': ['custom-properties', 'declarations'],
'order/properties-alphabetical-order': true,
- 'property-no-vendor-prefix': true,
- 'media-feature-name-no-vendor-prefix': true,
- 'at-rule-no-vendor-prefix': true,
- 'selector-no-vendor-prefix': true,
- 'max-nesting-depth': 3,
- 'selector-max-compound-selectors': 5,
- 'selector-class-pattern': [
+ /** Disable rules which conflict with Emotion */
+ 'function-no-unknown': null,
+ 'value-keyword-case': null,
+ 'function-name-case': null,
+ /** Enforce camel-case CSS variable names */
+ 'custom-property-pattern': [
'^[a-z][a-zA-Z0-9]+$',
{
message: 'Expected "%s" variable name to be lower camelCase',
},
],
+ 'selector-class-pattern': null,
+ 'annotation-no-unknown': null,
+ 'custom-property-empty-line-before': null,
+ 'block-no-empty': null,
+ 'length-zero-no-unit': true,
+ 'declaration-block-no-redundant-longhand-properties': null,
},
- plugins: ['stylelint-order'],
- ignoreFiles: ['**/*.snap'],
};