mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-28 07:47:40 +01:00
Implement redux.
This commit is contained in:
parent
9d8a2406e1
commit
5d4ff5cfa4
@ -1,14 +1,30 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import { useSelector } from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Link from 'components/Link';
|
||||||
|
import styles from './Header.module.css';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const user = useSelector(state => state.user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="container">
|
<header className={classNames(styles.header, 'container')}>
|
||||||
<h1>
|
<div className="row align-items-center">
|
||||||
<Link href="/">
|
<div className="col">
|
||||||
<a>umami</a>
|
<Link href="/" className={styles.title}>
|
||||||
|
umami
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</div>
|
||||||
|
{user && (
|
||||||
|
<div className="col">
|
||||||
|
<div className={styles.nav}>
|
||||||
|
<Link href="/">Dashboard</Link>
|
||||||
|
<Link href="/settings">Settings</Link>
|
||||||
|
<Link href="/logout">Logout</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
25
components/Header.module.css
Normal file
25
components/Header.module.css
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav > * {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
@ -3,9 +3,9 @@ import classNames from 'classnames';
|
|||||||
import NextLink from 'next/link';
|
import NextLink from 'next/link';
|
||||||
import styles from './Link.module.css';
|
import styles from './Link.module.css';
|
||||||
|
|
||||||
export default function Link({ href, className, children }) {
|
export default function Link({ className, children, ...props }) {
|
||||||
return (
|
return (
|
||||||
<NextLink href={href}>
|
<NextLink {...props}>
|
||||||
<a className={classNames(styles.link, className)}>{children}</a>
|
<a className={classNames(styles.link, className)}>{children}</a>
|
||||||
</NextLink>
|
</NextLink>
|
||||||
);
|
);
|
||||||
|
@ -20,7 +20,7 @@ export default function Login() {
|
|||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
|
|
||||||
const handleSubmit = async ({ username, password }) => {
|
const handleSubmit = async ({ username, password }) => {
|
||||||
const response = await post('/api/auth', { username, password });
|
const response = await post('/api/auth/login', { username, password });
|
||||||
|
|
||||||
if (response?.token) {
|
if (response?.token) {
|
||||||
await Router.push('/');
|
await Router.push('/');
|
||||||
|
@ -3,10 +3,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1000px) {
|
@media only screen and (max-width: 1000px) {
|
||||||
.container {
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container > div:last-child {
|
.container > div:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
min-height: 430px;
|
min-height: 430px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@ -76,12 +78,14 @@
|
|||||||
|
|
||||||
.body {
|
.body {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body:empty:before {
|
.body:empty:before {
|
||||||
content: 'No data available';
|
content: 'No data available';
|
||||||
display: block;
|
|
||||||
color: #b3b3b3;
|
color: #b3b3b3;
|
||||||
text-align: center;
|
position: absolute;
|
||||||
line-height: 50px;
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
9
components/Settings.js
Normal file
9
components/Settings.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Settings</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,3 +1,8 @@
|
|||||||
|
.container {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
@ -18,23 +18,31 @@ export default function WebsiteList() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.container}>
|
||||||
{data &&
|
{data &&
|
||||||
data.websites.map(({ website_id, label }) => (
|
data.websites.map(({ website_id, label }) => (
|
||||||
<div key={website_id} className={styles.website}>
|
<div key={website_id} className={styles.website}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h2>
|
<h2>
|
||||||
<Link href={`/website/${website_id}/${label}`} className={styles.title}>
|
<Link
|
||||||
|
href="/website/[...id]"
|
||||||
|
as={`/website/${website_id}/${label}`}
|
||||||
|
className={styles.title}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<Link href={`/website/${website_id}/${label}`} className={styles.details}>
|
<Link
|
||||||
|
href="/website/[...id]"
|
||||||
|
as={`/website/${website_id}/${label}`}
|
||||||
|
className={styles.details}
|
||||||
|
>
|
||||||
<Icon icon={<Arrow />} /> View details
|
<Icon icon={<Arrow />} /> View details
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<WebsiteChart key={website_id} title={label} websiteId={website_id} />
|
<WebsiteChart key={website_id} title={label} websiteId={website_id} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
.container {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.website {
|
.website {
|
||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
border-bottom: 1px solid #e1e1e1;
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
42
hooks/useUser.js
Normal file
42
hooks/useUser.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { updateUser } from 'redux/actions/user';
|
||||||
|
|
||||||
|
export async function fetchUser() {
|
||||||
|
const res = await fetch('/api/auth/verify');
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useUser() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const storeUser = useSelector(state => state.user);
|
||||||
|
const [loading, setLoading] = useState(!storeUser);
|
||||||
|
const [user, setUser] = useState(storeUser || null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
fetchUser().then(async user => {
|
||||||
|
if (!user) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dispatch(updateUser({ user: user }));
|
||||||
|
|
||||||
|
setUser(user);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { user, loading };
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import { parse } from 'cookie';
|
import { parse } from 'cookie';
|
||||||
import { verifySecureToken } from './crypto';
|
import { verifySecureToken } from './crypto';
|
||||||
|
import { AUTH_COOKIE_NAME } from './constants';
|
||||||
|
|
||||||
export default async req => {
|
export async function verifyAuthToken(req) {
|
||||||
const token = parse(req.headers.cookie || '')['umami.auth'];
|
const token = parse(req.headers.cookie || '')[AUTH_COOKIE_NAME];
|
||||||
|
|
||||||
return verifySecureToken(token);
|
return verifySecureToken(token);
|
||||||
};
|
}
|
||||||
|
1
lib/constants.js
Normal file
1
lib/constants.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const AUTH_COOKIE_NAME = 'umami.auth';
|
@ -285,6 +285,8 @@ export const refFilter = data =>
|
|||||||
data.filter(({ x }) => x !== '' && !x.startsWith('/') && !x.startsWith('#'));
|
data.filter(({ x }) => x !== '' && !x.startsWith('/') && !x.startsWith('#'));
|
||||||
|
|
||||||
export const deviceFilter = data => {
|
export const deviceFilter = data => {
|
||||||
|
if (data.length === 0) return [];
|
||||||
|
|
||||||
const devices = data.reduce(
|
const devices = data.reduce(
|
||||||
(obj, { x, y }) => {
|
(obj, { x, y }) => {
|
||||||
const [width] = x.split('x');
|
const [width] = x.split('x');
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import session from './session';
|
import { verifySession } from './session';
|
||||||
import auth from './auth';
|
import { verifyAuthToken } from './auth';
|
||||||
|
|
||||||
export function use(middleware) {
|
export function use(middleware) {
|
||||||
return (req, res) =>
|
return (req, res) =>
|
||||||
@ -18,7 +18,7 @@ export const useCors = use(cors());
|
|||||||
|
|
||||||
export const useSession = use(async (req, res, next) => {
|
export const useSession = use(async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
req.session = await session(req);
|
req.session = await verifySession(req);
|
||||||
} catch {
|
} catch {
|
||||||
return res.status(400).end();
|
return res.status(400).end();
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ export const useSession = use(async (req, res, next) => {
|
|||||||
|
|
||||||
export const useAuth = use(async (req, res, next) => {
|
export const useAuth = use(async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
req.auth = await auth(req);
|
req.auth = await verifyAuthToken(req);
|
||||||
} catch {
|
} catch {
|
||||||
return res.status(401).end();
|
return res.status(401).end();
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { getWebsite, getSession, createSession } from 'lib/db';
|
|||||||
import { getCountry, getDevice, getIpAddress } from 'lib/request';
|
import { getCountry, getDevice, getIpAddress } from 'lib/request';
|
||||||
import { uuid, isValidId, verifyToken } from 'lib/crypto';
|
import { uuid, isValidId, verifyToken } from 'lib/crypto';
|
||||||
|
|
||||||
export default async req => {
|
export async function verifySession(req) {
|
||||||
const { payload } = req.body;
|
const { payload } = req.body;
|
||||||
const { website: website_uuid, hostname, screen, language, session } = payload;
|
const { website: website_uuid, hostname, screen, language, session } = payload;
|
||||||
|
|
||||||
@ -51,4 +51,4 @@ export default async req => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "2.3.0",
|
"@prisma/client": "2.3.0",
|
||||||
|
"@reduxjs/toolkit": "^1.4.0",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"chart.js": "^2.9.3",
|
"chart.js": "^2.9.3",
|
||||||
@ -60,9 +61,12 @@
|
|||||||
"promise-polyfill": "^8.1.3",
|
"promise-polyfill": "^8.1.3",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
|
"react-redux": "^7.2.1",
|
||||||
"react-simple-maps": "^2.1.2",
|
"react-simple-maps": "^2.1.2",
|
||||||
"react-spring": "^8.0.27",
|
"react-spring": "^8.0.27",
|
||||||
"react-tooltip": "^4.2.7",
|
"react-tooltip": "^4.2.7",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
"request-ip": "^2.1.3",
|
"request-ip": "^2.1.3",
|
||||||
"tinycolor2": "^1.4.1",
|
"tinycolor2": "^1.4.1",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { useStore } from 'redux/store';
|
||||||
import 'styles/bootstrap-grid.css';
|
import 'styles/bootstrap-grid.css';
|
||||||
import 'styles/index.css';
|
import 'styles/index.css';
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
return <Component {...pageProps} />;
|
const store = useStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { serialize } from 'cookie';
|
import { serialize } from 'cookie';
|
||||||
import { checkPassword, createSecureToken } from 'lib/crypto';
|
import { checkPassword, createSecureToken } from 'lib/crypto';
|
||||||
import { getAccount } from 'lib/db';
|
import { getAccount } from 'lib/db';
|
||||||
|
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
@ -10,7 +11,7 @@ export default async (req, res) => {
|
|||||||
if (account && (await checkPassword(password, account.password))) {
|
if (account && (await checkPassword(password, account.password))) {
|
||||||
const { user_id, username, is_admin } = account;
|
const { user_id, username, is_admin } = account;
|
||||||
const token = await createSecureToken({ user_id, username, is_admin });
|
const token = await createSecureToken({ user_id, username, is_admin });
|
||||||
const cookie = serialize('umami.auth', token, {
|
const cookie = serialize(AUTH_COOKIE_NAME, token, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
maxAge: 60 * 60 * 24 * 365,
|
maxAge: 60 * 60 * 24 * 365,
|
16
pages/api/auth/logout.js
Normal file
16
pages/api/auth/logout.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { serialize } from 'cookie';
|
||||||
|
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const cookie = serialize(AUTH_COOKIE_NAME, '', {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.statusCode = 303;
|
||||||
|
res.setHeader('Set-Cookie', [cookie]);
|
||||||
|
res.setHeader('Location', '/login');
|
||||||
|
|
||||||
|
return res.end();
|
||||||
|
};
|
11
pages/api/auth/verify.js
Normal file
11
pages/api/auth/verify.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.auth) {
|
||||||
|
return res.status(200).json(req.auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(401).end();
|
||||||
|
};
|
@ -1,33 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { parse } from 'cookie';
|
|
||||||
import Layout from 'components/Layout';
|
import Layout from 'components/Layout';
|
||||||
import { verifySecureToken } from 'lib/crypto';
|
import WebsiteList from 'components/WebsiteList';
|
||||||
import WebsiteList from '../components/WebsiteList';
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const { loading } = useUser();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomePage({ username }) {
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<WebsiteList />
|
<WebsiteList />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps({ req, res }) {
|
|
||||||
const token = parse(req.headers.cookie || '')['umami.auth'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = await verifySecureToken(token);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
...payload,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
res.statusCode = 303;
|
|
||||||
res.setHeader('Location', '/login');
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { props: {} };
|
|
||||||
}
|
|
||||||
|
@ -1,27 +1,9 @@
|
|||||||
import React from 'react';
|
import { useEffect } from 'react';
|
||||||
import { serialize } from 'cookie';
|
|
||||||
import Layout from 'components/Layout';
|
|
||||||
|
|
||||||
export default function LogoutPage() {
|
export default function LogoutPage() {
|
||||||
return (
|
useEffect(() => {
|
||||||
<Layout title="Logout">
|
fetch('/api/auth/logout').then(() => (window.location.href = '/login'));
|
||||||
<h2>You've successfully logged out..</h2>
|
}, []);
|
||||||
</Layout>
|
|
||||||
);
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
export async function getServerSideProps({ res }) {
|
|
||||||
const cookie = serialize('umami.auth', '', {
|
|
||||||
path: '/',
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.statusCode = 303;
|
|
||||||
res.setHeader('Set-Cookie', [cookie]);
|
|
||||||
res.setHeader('Location', '/login');
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
|
|
||||||
return { props: {} };
|
|
||||||
}
|
}
|
||||||
|
18
pages/settings.js
Normal file
18
pages/settings.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Layout from 'components/Layout';
|
||||||
|
import Settings from 'components/Settings';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const { loading } = useUser();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Settings />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
@ -1,13 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Layout from 'components/Layout';
|
import Layout from 'components/Layout';
|
||||||
import WebsiteDetails from '../../components/WebsiteDetails';
|
import WebsiteDetails from 'components/WebsiteDetails';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function DetailsPage() {
|
export default function DetailsPage() {
|
||||||
|
const { loading } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
if (!id) {
|
if (!id || loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
16
redux/actions/user.js
Normal file
16
redux/actions/user.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const user = createSlice({
|
||||||
|
name: 'user',
|
||||||
|
initialState: null,
|
||||||
|
reducers: {
|
||||||
|
updateUser(state, action) {
|
||||||
|
state = action.payload;
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { updateUser } = user.actions;
|
||||||
|
|
||||||
|
export default user.reducer;
|
4
redux/reducers.js
Normal file
4
redux/reducers.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import user from './actions/user';
|
||||||
|
|
||||||
|
export default combineReducers({ user });
|
40
redux/store.js
Normal file
40
redux/store.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import rootReducer from './reducers';
|
||||||
|
|
||||||
|
let store;
|
||||||
|
|
||||||
|
export function getStore(preloadedState) {
|
||||||
|
return configureStore({
|
||||||
|
reducer: rootReducer,
|
||||||
|
middleware: [thunk],
|
||||||
|
preloadedState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initializeStore = preloadedState => {
|
||||||
|
let _store = store ?? getStore(preloadedState);
|
||||||
|
|
||||||
|
// After navigating to a page with an initial Redux state, merge that state
|
||||||
|
// with the current state in the store, and create a new store
|
||||||
|
if (preloadedState && store) {
|
||||||
|
_store = getStore({
|
||||||
|
...store.getState(),
|
||||||
|
...preloadedState,
|
||||||
|
});
|
||||||
|
// Reset the current store
|
||||||
|
store = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For SSG and SSR always create a new store
|
||||||
|
if (typeof window === 'undefined') return _store;
|
||||||
|
// Create the store once in the client
|
||||||
|
if (!store) store = _store;
|
||||||
|
|
||||||
|
return _store;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useStore(initialState) {
|
||||||
|
return useMemo(() => initializeStore(initialState), [initialState]);
|
||||||
|
}
|
@ -60,11 +60,10 @@ select {
|
|||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 0 20px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
58
yarn.lock
58
yarn.lock
@ -1102,6 +1102,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.5.5":
|
||||||
|
version "7.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.0.tgz#f10245877042a815e07f7e693faff0ae9d3a2aac"
|
||||||
|
integrity sha512-qArkXsjJq7H+T86WrIFV0Fnu/tNOkZ4cgXmjkzAu3b/58D5mFIO8JH/y77t7C9q0OdDRdh9s7Ue5GasYssxtXw==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.10.4", "@babel/template@^7.7.4":
|
"@babel/template@^7.10.4", "@babel/template@^7.7.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
||||||
@ -1217,6 +1224,16 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
pkg-up "^3.1.0"
|
pkg-up "^3.1.0"
|
||||||
|
|
||||||
|
"@reduxjs/toolkit@^1.4.0":
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.4.0.tgz#ee2e2384cc3d1d76780d844b9c2da3580d32710d"
|
||||||
|
integrity sha512-hkxQwVx4BNVRsYdxjNF6cAseRmtrkpSlcgJRr3kLUcHPIAMZAmMJkXmHh/eUEGTMqPzsYpJLM7NN2w9fxQDuGw==
|
||||||
|
dependencies:
|
||||||
|
immer "^7.0.3"
|
||||||
|
redux "^4.0.0"
|
||||||
|
redux-thunk "^2.3.0"
|
||||||
|
reselect "^4.0.0"
|
||||||
|
|
||||||
"@rollup/plugin-buble@^0.21.3":
|
"@rollup/plugin-buble@^0.21.3":
|
||||||
version "0.21.3"
|
version "0.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-buble/-/plugin-buble-0.21.3.tgz#1649a915b1d051a4f430d40e7734a7f67a69b33e"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-buble/-/plugin-buble-0.21.3.tgz#1649a915b1d051a4f430d40e7734a7f67a69b33e"
|
||||||
@ -4529,6 +4546,11 @@ image-size@~0.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
|
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
|
||||||
integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=
|
integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=
|
||||||
|
|
||||||
|
immer@^7.0.3:
|
||||||
|
version "7.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.7.tgz#9dfe713d49bf871cc59aedfce59b1992fa37a977"
|
||||||
|
integrity sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw==
|
||||||
|
|
||||||
import-fresh@^2.0.0:
|
import-fresh@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
|
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
|
||||||
@ -7328,11 +7350,22 @@ react-fast-compare@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1:
|
react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
|
react-redux@^7.2.1:
|
||||||
|
version "7.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
|
||||||
|
integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.5"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-is "^16.9.0"
|
||||||
|
|
||||||
react-refresh@0.8.3:
|
react-refresh@0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
@ -7445,6 +7478,19 @@ redent@^3.0.0:
|
|||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
|
redux-thunk@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||||
|
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
||||||
|
|
||||||
|
redux@^4.0.0, redux@^4.0.5:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||||
|
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
symbol-observable "^1.2.0"
|
||||||
|
|
||||||
reflect.ownkeys@^0.2.0:
|
reflect.ownkeys@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
||||||
@ -7634,6 +7680,11 @@ require-relative@^0.8.7:
|
|||||||
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
|
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
|
||||||
integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=
|
integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=
|
||||||
|
|
||||||
|
reselect@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
|
||||||
|
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
|
||||||
|
|
||||||
resolve-from@^3.0.0:
|
resolve-from@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
||||||
@ -8585,6 +8636,11 @@ svgo@^1.0.0, svgo@^1.2.2:
|
|||||||
unquote "~1.1.1"
|
unquote "~1.1.1"
|
||||||
util.promisify "~1.0.0"
|
util.promisify "~1.0.0"
|
||||||
|
|
||||||
|
symbol-observable@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
|
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||||
|
|
||||||
table@^5.2.3, table@^5.4.6:
|
table@^5.2.3, table@^5.4.6:
|
||||||
version "5.4.6"
|
version "5.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
||||||
|
Loading…
Reference in New Issue
Block a user