mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 17:55:08 +01:00
Replaced redux with zustand. Fixed login issue, closes #980.
This commit is contained in:
parent
7071f5fba5
commit
9937caa569
@ -1,23 +1,23 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { setDateRange } from 'redux/actions/websites';
|
import useStore from 'store/queries';
|
||||||
|
import { setDateRange } from 'store/websites';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Refresh from 'assets/redo.svg';
|
import Refresh from 'assets/redo.svg';
|
||||||
import Dots from 'assets/ellipsis-h.svg';
|
import Dots from 'assets/ellipsis-h.svg';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
|
|
||||||
function RefreshButton({ websiteId }) {
|
function RefreshButton({ websiteId }) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [dateRange] = useDateRange(websiteId);
|
const [dateRange] = useDateRange(websiteId);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
|
const selector = useCallback(state => state[`/api/website/${websiteId}/stats`], [websiteId]);
|
||||||
|
const completed = useStore(selector);
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (dateRange) {
|
if (dateRange) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
dispatch(setDateRange(websiteId, dateRange));
|
setDateRange(websiteId, dateRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,11 +10,12 @@ import FormLayout, {
|
|||||||
FormRow,
|
FormRow,
|
||||||
} from 'components/layout/FormLayout';
|
} from 'components/layout/FormLayout';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import Logo from 'assets/logo.svg';
|
|
||||||
import styles from './LoginForm.module.css';
|
|
||||||
import usePost from 'hooks/usePost';
|
import usePost from 'hooks/usePost';
|
||||||
import { setItem } from 'lib/web';
|
import { setItem } from 'lib/web';
|
||||||
import { AUTH_TOKEN } from '../../lib/constants';
|
import { AUTH_TOKEN } from 'lib/constants';
|
||||||
|
import { setUser } from 'store/app';
|
||||||
|
import Logo from 'assets/logo.svg';
|
||||||
|
import styles from './LoginForm.module.css';
|
||||||
|
|
||||||
const validate = ({ username, password }) => {
|
const validate = ({ username, password }) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
@ -43,6 +44,8 @@ export default function LoginForm() {
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setItem(AUTH_TOKEN, data.token);
|
setItem(AUTH_TOKEN, data.token);
|
||||||
|
|
||||||
|
setUser(data.user);
|
||||||
|
|
||||||
return router.push('/');
|
return router.push('/');
|
||||||
} else {
|
} else {
|
||||||
setMessage(
|
setMessage(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from 'components/common/Link';
|
import Link from 'components/common/Link';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
@ -14,9 +13,10 @@ import styles from './Header.module.css';
|
|||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import XMark from 'assets/xmark.svg';
|
import XMark from 'assets/xmark.svg';
|
||||||
import Bars from 'assets/bars.svg';
|
import Bars from 'assets/bars.svg';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const user = useSelector(state => state.user);
|
const { user } = useUser();
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
const { dir } = useLocale();
|
const { dir } = useLocale();
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import MenuLayout from 'components/layout/MenuLayout';
|
import MenuLayout from 'components/layout/MenuLayout';
|
||||||
import WebsiteSettings from '../settings/WebsiteSettings';
|
import WebsiteSettings from 'components/settings/WebsiteSettings';
|
||||||
import AccountSettings from '../settings/AccountSettings';
|
import AccountSettings from 'components/settings/AccountSettings';
|
||||||
import ProfileSettings from '../settings/ProfileSettings';
|
import ProfileSettings from 'components/settings/ProfileSettings';
|
||||||
import { useSelector } from 'react-redux';
|
import useUser from 'hooks/useUser';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
const WEBSITES = '/settings';
|
const WEBSITES = '/settings';
|
||||||
const ACCOUNTS = '/settings/accounts';
|
const ACCOUNTS = '/settings/accounts';
|
||||||
const PROFILE = '/settings/profile';
|
const PROFILE = '/settings/profile';
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const user = useSelector(state => state.user);
|
const { user } = useUser();
|
||||||
const [option, setOption] = useState(WEBSITES);
|
const [option, setOption] = useState(WEBSITES);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { pathname } = router;
|
const { pathname } = router;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -13,11 +12,12 @@ import Button from 'components/common/Button';
|
|||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
import ChevronDown from 'assets/chevron-down.svg';
|
import ChevronDown from 'assets/chevron-down.svg';
|
||||||
import styles from './TestConsole.module.css';
|
import styles from './TestConsole.module.css';
|
||||||
|
|
||||||
export default function TestConsole() {
|
export default function TestConsole() {
|
||||||
const user = useSelector(state => state.user);
|
const { user } = useUser();
|
||||||
const [website, setWebsite] = useState();
|
const [website, setWebsite] = useState();
|
||||||
const [show, setShow] = useState(true);
|
const [show, setShow] = useState(true);
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
|
@ -14,6 +14,14 @@ export default function DateRangeSetting() {
|
|||||||
const { startDate, endDate, value } = dateRange;
|
const { startDate, endDate, value } = dateRange;
|
||||||
const options = filterOptions.filter(e => e.value !== 'all');
|
const options = filterOptions.filter(e => e.value !== 'all');
|
||||||
|
|
||||||
|
function handleChange(value) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
setDateRange(getDateRange(value, locale));
|
||||||
|
} else {
|
||||||
|
setDateRange(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
|
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
|
||||||
}
|
}
|
||||||
@ -25,7 +33,7 @@ export default function DateRangeSetting() {
|
|||||||
value={value}
|
value={value}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
onChange={setDateRange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import Modal from 'components/common/Modal';
|
import Modal from 'components/common/Modal';
|
||||||
@ -11,12 +10,12 @@ import Dots from 'assets/ellipsis-h.svg';
|
|||||||
import styles from './ProfileSettings.module.css';
|
import styles from './ProfileSettings.module.css';
|
||||||
import DateRangeSetting from './DateRangeSetting';
|
import DateRangeSetting from './DateRangeSetting';
|
||||||
import useEscapeKey from 'hooks/useEscapeKey';
|
import useEscapeKey from 'hooks/useEscapeKey';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function ProfileSettings() {
|
export default function ProfileSettings() {
|
||||||
const user = useSelector(state => state.user);
|
const { user } = useUser();
|
||||||
const [changePassword, setChangePassword] = useState(false);
|
const [changePassword, setChangePassword] = useState(false);
|
||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
const { user_id } = user;
|
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
setChangePassword(false);
|
setChangePassword(false);
|
||||||
@ -27,6 +26,12 @@ export default function ProfileSettings() {
|
|||||||
setChangePassword(false);
|
setChangePassword(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user_id, username } = user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
@ -41,7 +46,7 @@ export default function ProfileSettings() {
|
|||||||
<dt>
|
<dt>
|
||||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||||
</dt>
|
</dt>
|
||||||
<dd>{user.username}</dd>
|
<dd>{username}</dd>
|
||||||
<dt>
|
<dt>
|
||||||
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
|
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
|
||||||
</dt>
|
</dt>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import MenuButton from 'components/common/MenuButton';
|
import MenuButton from 'components/common/MenuButton';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
@ -9,9 +8,10 @@ import Chevron from 'assets/chevron-down.svg';
|
|||||||
import styles from './UserButton.module.css';
|
import styles from './UserButton.module.css';
|
||||||
import { removeItem } from 'lib/web';
|
import { removeItem } from 'lib/web';
|
||||||
import { AUTH_TOKEN } from 'lib/constants';
|
import { AUTH_TOKEN } from 'lib/constants';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function UserButton() {
|
export default function UserButton() {
|
||||||
const user = useSelector(state => state.user);
|
const { user } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const menuOptions = [
|
const menuOptions = [
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from 'components/common/Link';
|
import Link from 'components/common/Link';
|
||||||
import Table from 'components/common/Table';
|
import Table from 'components/common/Table';
|
||||||
@ -23,10 +22,11 @@ import Plus from 'assets/plus.svg';
|
|||||||
import Code from 'assets/code.svg';
|
import Code from 'assets/code.svg';
|
||||||
import LinkIcon from 'assets/link.svg';
|
import LinkIcon from 'assets/link.svg';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
import styles from './WebsiteSettings.module.css';
|
import styles from './WebsiteSettings.module.css';
|
||||||
|
|
||||||
export default function WebsiteSettings() {
|
export default function WebsiteSettings() {
|
||||||
const user = useSelector(state => state.user);
|
const { user } = useUser();
|
||||||
const [editWebsite, setEditWebsite] = useState();
|
const [editWebsite, setEditWebsite] = useState();
|
||||||
const [resetWebsite, setResetWebsite] = useState();
|
const [resetWebsite, setResetWebsite] = useState();
|
||||||
const [deleteWebsite, setDeleteWebsite] = useState();
|
const [deleteWebsite, setDeleteWebsite] = useState();
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useCallback } from 'react';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
import { getItem, setItem } from 'lib/web';
|
import { getItem, setItem } from 'lib/web';
|
||||||
import { setDateRange } from '../redux/actions/websites';
|
|
||||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||||
import useForceUpdate from './useForceUpdate';
|
import useForceUpdate from './useForceUpdate';
|
||||||
import useLocale from './useLocale';
|
import useLocale from './useLocale';
|
||||||
|
import useStore, { setDateRange } from 'store/websites';
|
||||||
|
|
||||||
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
|
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
|
const selector = useCallback(state => state?.[websiteId]?.dateRange, [websiteId]);
|
||||||
|
const dateRange = useStore(selector);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
const globalDefault = getItem(DATE_RANGE_CONFIG);
|
const globalDefault = getItem(DATE_RANGE_CONFIG);
|
||||||
@ -28,13 +28,12 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDateRange(values) {
|
function saveDateRange(dateRange) {
|
||||||
const { value } = values;
|
|
||||||
|
|
||||||
if (websiteId) {
|
if (websiteId) {
|
||||||
dispatch(setDateRange(websiteId, values));
|
setDateRange(websiteId, dateRange);
|
||||||
} else {
|
} else {
|
||||||
setItem(DATE_RANGE_CONFIG, value === 'custom' ? values : value);
|
const { value } = dateRange;
|
||||||
|
setItem(DATE_RANGE_CONFIG, value === 'custom' ? dateRange : value);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { updateQuery } from 'redux/actions/queries';
|
import { saveQuery } from 'store/queries';
|
||||||
|
|
||||||
export default function useFetch(url, options = {}, update = []) {
|
export default function useFetch(url, options = {}, update = []) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [response, setResponse] = useState();
|
const [response, setResponse] = useState();
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
const [loading, setLoadiing] = useState(false);
|
const [loading, setLoadiing] = useState(false);
|
||||||
@ -21,7 +19,7 @@ export default function useFetch(url, options = {}, update = []) {
|
|||||||
|
|
||||||
const { data, status, ok } = await get(`${basePath}${url}`, params, headers);
|
const { data, status, ok } = await get(`${basePath}${url}`, params, headers);
|
||||||
|
|
||||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
await saveQuery(url, { time: performance.now() - time, completed: Date.now() });
|
||||||
|
|
||||||
if (status >= 400) {
|
if (status >= 400) {
|
||||||
setError(data);
|
setError(data);
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { setLocale } from 'redux/actions/app';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get, setItem } from 'lib/web';
|
import { get, setItem } from 'lib/web';
|
||||||
import { LOCALE_CONFIG } from 'lib/constants';
|
import { LOCALE_CONFIG } from 'lib/constants';
|
||||||
import { getDateLocale, getTextDirection } from 'lib/lang';
|
import { getDateLocale, getTextDirection } from 'lib/lang';
|
||||||
|
import useStore, { setLocale } from 'store/app';
|
||||||
import useForceUpdate from 'hooks/useForceUpdate';
|
import useForceUpdate from 'hooks/useForceUpdate';
|
||||||
import enUS from 'public/messages/en-US.json';
|
import enUS from 'public/messages/en-US.json';
|
||||||
|
|
||||||
@ -12,9 +11,10 @@ const messages = {
|
|||||||
'en-US': enUS,
|
'en-US': enUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selector = state => state.locale;
|
||||||
|
|
||||||
export default function useLocale() {
|
export default function useLocale() {
|
||||||
const locale = useSelector(state => state.app.locale);
|
const locale = useStore(selector);
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
const dir = getTextDirection(locale);
|
const dir = getTextDirection(locale);
|
||||||
@ -36,7 +36,7 @@ export default function useLocale() {
|
|||||||
setItem(LOCALE_CONFIG, value);
|
setItem(LOCALE_CONFIG, value);
|
||||||
|
|
||||||
if (locale !== value) {
|
if (locale !== value) {
|
||||||
dispatch(setLocale(value));
|
setLocale(value);
|
||||||
} else {
|
} else {
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { updateUser } from 'redux/actions/user';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
|
|
||||||
export default function useRequireLogin() {
|
export default function useRequireLogin() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dispatch = useDispatch();
|
const { user, setUser } = useUser();
|
||||||
const storeUser = useSelector(state => state.user);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(!storeUser);
|
|
||||||
const [user, setUser] = useState(storeUser);
|
|
||||||
|
|
||||||
async function loadUser() {
|
async function loadUser() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -20,9 +17,8 @@ export default function useRequireLogin() {
|
|||||||
return router.push('/login');
|
return router.push('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
await dispatch(updateUser(data));
|
setUser(data);
|
||||||
|
|
||||||
setUser(user);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { setShareToken } from 'redux/actions/app';
|
import useStore, { setShareToken } from 'store/app';
|
||||||
|
|
||||||
|
const selector = state => state.shareToken;
|
||||||
|
|
||||||
export default function useShareToken(shareId) {
|
export default function useShareToken(shareId) {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const dispatch = useDispatch();
|
const shareToken = useStore(selector);
|
||||||
const shareToken = useSelector(state => state.app.shareToken);
|
|
||||||
|
|
||||||
async function loadToken(id) {
|
async function loadToken(id) {
|
||||||
const { data } = await get(`${basePath}/api/share/${id}`);
|
const { data } = await get(`${basePath}/api/share/${id}`);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
dispatch(setShareToken(data));
|
setShareToken(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useEffect } from 'react';
|
||||||
import { setTheme } from 'redux/actions/app';
|
import useStore, { setTheme } from 'store/app';
|
||||||
import { getItem, setItem } from 'lib/web';
|
import { getItem, setItem } from 'lib/web';
|
||||||
import { THEME_CONFIG } from 'lib/constants';
|
import { THEME_CONFIG } from 'lib/constants';
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
const selector = state => state.theme;
|
||||||
|
|
||||||
export default function useTheme() {
|
export default function useTheme() {
|
||||||
const defaultTheme =
|
const defaultTheme =
|
||||||
@ -11,12 +12,11 @@ export default function useTheme() {
|
|||||||
? 'dark'
|
? 'dark'
|
||||||
: 'light'
|
: 'light'
|
||||||
: 'light';
|
: 'light';
|
||||||
const theme = useSelector(state => state.app.theme || getItem(THEME_CONFIG) || defaultTheme);
|
const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
function saveTheme(value) {
|
function saveTheme(value) {
|
||||||
setItem(THEME_CONFIG, value);
|
setItem(THEME_CONFIG, value);
|
||||||
dispatch(setTheme(value));
|
setTheme(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
9
hooks/useUser.js
Normal file
9
hooks/useUser.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import useStore, { setUser } from 'store/app';
|
||||||
|
|
||||||
|
const selector = state => state.user;
|
||||||
|
|
||||||
|
export default function useUser() {
|
||||||
|
const user = useStore(selector);
|
||||||
|
|
||||||
|
return { user, setUser };
|
||||||
|
}
|
@ -1,12 +1,10 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import useStore, { checkVersion } from 'store/version';
|
||||||
import { checkVersion } from 'redux/actions/app';
|
|
||||||
import { VERSION_CHECK } from 'lib/constants';
|
import { VERSION_CHECK } from 'lib/constants';
|
||||||
import { getItem, setItem } from 'lib/web';
|
import { getItem, setItem } from 'lib/web';
|
||||||
|
|
||||||
export default function useVersion(check) {
|
export default function useVersion(check) {
|
||||||
const dispatch = useDispatch();
|
const versions = useStore();
|
||||||
const versions = useSelector(state => state.app.versions);
|
|
||||||
const checked = versions.latest === getItem(VERSION_CHECK)?.version;
|
const checked = versions.latest === getItem(VERSION_CHECK)?.version;
|
||||||
|
|
||||||
const updateCheck = useCallback(() => {
|
const updateCheck = useCallback(() => {
|
||||||
@ -15,7 +13,7 @@ export default function useVersion(check) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (check && !versions.latest) {
|
if (check && !versions.latest) {
|
||||||
dispatch(checkVersion());
|
checkVersion();
|
||||||
}
|
}
|
||||||
}, [versions, check]);
|
}, [versions, check]);
|
||||||
|
|
||||||
|
10
lib/web.js
10
lib/web.js
@ -65,9 +65,17 @@ export const setItem = (key, data, session) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function tryParse(value) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getItem = (key, session) =>
|
export const getItem = (key, session) =>
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
? JSON.parse((session ? sessionStorage : localStorage).getItem(key))
|
? tryParse((session ? sessionStorage : localStorage).getItem(key))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
export const removeItem = (key, session) => {
|
export const removeItem = (key, session) => {
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
"detect-browser": "^5.2.0",
|
"detect-browser": "^5.2.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"immer": "^9.0.6",
|
"immer": "^9.0.12",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.0.1",
|
||||||
"is-localhost-ip": "^1.4.0",
|
"is-localhost-ip": "^1.4.0",
|
||||||
"isbot": "^3.2.2",
|
"isbot": "^3.2.2",
|
||||||
@ -92,7 +92,8 @@
|
|||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"timezone-support": "^2.0.2",
|
"timezone-support": "^2.0.2",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2",
|
||||||
|
"zustand": "^3.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^4.2.29",
|
"@formatjs/cli": "^4.2.29",
|
||||||
|
@ -2,8 +2,6 @@ import React from 'react';
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { useStore } from 'redux/store';
|
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import 'styles/variables.css';
|
import 'styles/variables.css';
|
||||||
import 'styles/bootstrap-grid.css';
|
import 'styles/bootstrap-grid.css';
|
||||||
@ -24,11 +22,10 @@ const Intl = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
const store = useStore();
|
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Intl>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
||||||
@ -41,9 +38,7 @@ export default function App({ Component, pageProps }) {
|
|||||||
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
<Intl>
|
<Component {...pageProps} />
|
||||||
<Component {...pageProps} />
|
</Intl>
|
||||||
</Intl>
|
|
||||||
</Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,10 @@ 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 user = { user_id, username, is_admin };
|
||||||
|
const token = await createSecureToken(user);
|
||||||
|
|
||||||
return ok(res, { token });
|
return ok(res, { token, user });
|
||||||
}
|
}
|
||||||
|
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get } from 'lib/web';
|
import { removeItem } from 'lib/web';
|
||||||
import { updateUser } from 'redux/actions/user';
|
import { AUTH_TOKEN } from 'lib/constants';
|
||||||
|
import { setUser } from 'store/app';
|
||||||
|
|
||||||
export default function LogoutPage() {
|
export default function LogoutPage() {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { basePath } = router;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(updateUser(null));
|
removeItem(AUTH_TOKEN);
|
||||||
get(`${basePath}/api/auth/logout`).then(() => router.push('/login'));
|
router.push('/login');
|
||||||
|
|
||||||
|
return () => setUser(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
import { getItem } from 'lib/web';
|
|
||||||
import {
|
|
||||||
DEFAULT_LOCALE,
|
|
||||||
DEFAULT_THEME,
|
|
||||||
LOCALE_CONFIG,
|
|
||||||
THEME_CONFIG,
|
|
||||||
VERSION_CHECK,
|
|
||||||
} from 'lib/constants';
|
|
||||||
import semver from 'semver';
|
|
||||||
|
|
||||||
const app = createSlice({
|
|
||||||
name: 'app',
|
|
||||||
initialState: {
|
|
||||||
locale: getItem(LOCALE_CONFIG) || DEFAULT_LOCALE,
|
|
||||||
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
|
||||||
versions: {
|
|
||||||
current: process.env.VERSION,
|
|
||||||
latest: null,
|
|
||||||
hasUpdate: false,
|
|
||||||
},
|
|
||||||
shareToken: null,
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
setLocale(state, action) {
|
|
||||||
state.locale = action.payload;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
setTheme(state, action) {
|
|
||||||
state.theme = action.payload;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
setVersions(state, action) {
|
|
||||||
state.versions = action.payload;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
setShareToken(state, action) {
|
|
||||||
state.shareToken = action.payload;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setLocale, setTheme, setVersions, setShareToken } = app.actions;
|
|
||||||
|
|
||||||
export default app.reducer;
|
|
||||||
|
|
||||||
export function checkVersion() {
|
|
||||||
return async (dispatch, getState) => {
|
|
||||||
const {
|
|
||||||
app: {
|
|
||||||
versions: { current },
|
|
||||||
},
|
|
||||||
} = getState();
|
|
||||||
|
|
||||||
const data = await fetch('https://api.github.com/repos/mikecao/umami/releases/latest', {
|
|
||||||
method: 'get',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/vnd.github.v3+json',
|
|
||||||
},
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tag_name } = data;
|
|
||||||
|
|
||||||
const latest = tag_name.startsWith('v') ? tag_name.slice(1) : tag_name;
|
|
||||||
const lastCheck = getItem(VERSION_CHECK);
|
|
||||||
const hasUpdate = latest && semver.gt(latest, current) && lastCheck?.version !== latest;
|
|
||||||
|
|
||||||
return dispatch(
|
|
||||||
setVersions({
|
|
||||||
current,
|
|
||||||
latest,
|
|
||||||
hasUpdate,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
const queries = createSlice({
|
|
||||||
name: 'queries',
|
|
||||||
initialState: {},
|
|
||||||
reducers: {
|
|
||||||
updateQuery(state, action) {
|
|
||||||
const { url, ...data } = action.payload;
|
|
||||||
state[url] = data;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { updateQuery } = queries.actions;
|
|
||||||
|
|
||||||
export default queries.reducer;
|
|
@ -1,16 +0,0 @@
|
|||||||
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;
|
|
@ -1,29 +0,0 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
const websites = createSlice({
|
|
||||||
name: 'websites',
|
|
||||||
initialState: {},
|
|
||||||
reducers: {
|
|
||||||
updateWebsites(state, action) {
|
|
||||||
state = action.payload;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
updateWebsite(state, action) {
|
|
||||||
const { websiteId, ...data } = action.payload;
|
|
||||||
state[websiteId] = data;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { updateWebsites, updateWebsite } = websites.actions;
|
|
||||||
|
|
||||||
export default websites.reducer;
|
|
||||||
|
|
||||||
export function setDateRange(websiteId, dateRange) {
|
|
||||||
return dispatch => {
|
|
||||||
return dispatch(
|
|
||||||
updateWebsite({ websiteId, dateRange: { ...dateRange, modified: Date.now() } }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { combineReducers } from 'redux';
|
|
||||||
import app from './actions/app';
|
|
||||||
import user from './actions/user';
|
|
||||||
import websites from './actions/websites';
|
|
||||||
import queries from './actions/queries';
|
|
||||||
|
|
||||||
export default combineReducers({ app, user, websites, queries });
|
|
@ -1,40 +0,0 @@
|
|||||||
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]);
|
|
||||||
}
|
|
30
store/app.js
Normal file
30
store/app.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import create from 'zustand';
|
||||||
|
import { DEFAULT_LOCALE, DEFAULT_THEME, LOCALE_CONFIG, THEME_CONFIG } from 'lib/constants';
|
||||||
|
import { getItem } from 'lib/web';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
locale: getItem(LOCALE_CONFIG) || DEFAULT_LOCALE,
|
||||||
|
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
||||||
|
shareToken: null,
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = create(() => ({ ...initialState }));
|
||||||
|
|
||||||
|
export function setTheme(theme) {
|
||||||
|
store.setState({ theme });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocale(locale) {
|
||||||
|
store.setState({ locale });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setShareToken(shareToken) {
|
||||||
|
store.setState({ shareToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUser(user) {
|
||||||
|
store.setState({ user });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store;
|
9
store/queries.js
Normal file
9
store/queries.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import create from 'zustand';
|
||||||
|
|
||||||
|
const store = create(() => ({}));
|
||||||
|
|
||||||
|
export function saveQuery(url, data) {
|
||||||
|
store.setState({ [url]: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store;
|
54
store/version.js
Normal file
54
store/version.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import create from 'zustand';
|
||||||
|
import produce from 'immer';
|
||||||
|
import semver from 'semver';
|
||||||
|
import { VERSION_CHECK } from 'lib/constants';
|
||||||
|
import { getItem } from 'lib/web';
|
||||||
|
|
||||||
|
const REPO_URL = 'https://api.github.com/repos/mikecao/umami/releases/latest';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
current: process.env.VERSION,
|
||||||
|
latest: null,
|
||||||
|
hasUpdate: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = create(() => ({ ...initialState }));
|
||||||
|
|
||||||
|
export async function checkVersion() {
|
||||||
|
const { current } = store.getState();
|
||||||
|
|
||||||
|
const data = await fetch(REPO_URL, {
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.github.v3+json',
|
||||||
|
},
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setState(
|
||||||
|
produce(state => {
|
||||||
|
const { tag_name } = data;
|
||||||
|
|
||||||
|
const latest = tag_name.startsWith('v') ? tag_name.slice(1) : tag_name;
|
||||||
|
const lastCheck = getItem(VERSION_CHECK);
|
||||||
|
const hasUpdate = latest && semver.gt(latest, current) && lastCheck?.version !== latest;
|
||||||
|
|
||||||
|
state.current = current;
|
||||||
|
state.latest = latest;
|
||||||
|
state.hasUpdate = hasUpdate;
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store;
|
20
store/websites.js
Normal file
20
store/websites.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import create from 'zustand';
|
||||||
|
import produce from 'immer';
|
||||||
|
|
||||||
|
const store = create(() => ({}));
|
||||||
|
|
||||||
|
export function setDateRange(websiteId, dateRange) {
|
||||||
|
store.setState(
|
||||||
|
produce(state => {
|
||||||
|
if (!state[websiteId]) {
|
||||||
|
state[websiteId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
state[websiteId].dateRange = { ...dateRange, modified: Date.now() };
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store;
|
10
yarn.lock
10
yarn.lock
@ -4066,6 +4066,11 @@ ignore@^5.1.4, ignore@^5.1.8:
|
|||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
|
||||||
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
|
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
|
||||||
|
|
||||||
|
immer@^9.0.12:
|
||||||
|
version "9.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20"
|
||||||
|
integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==
|
||||||
|
|
||||||
immer@^9.0.6:
|
immer@^9.0.6:
|
||||||
version "9.0.6"
|
version "9.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
||||||
@ -7351,6 +7356,11 @@ yocto-queue@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zustand@^3.7.0:
|
||||||
|
version "3.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.0.tgz#a5c68fb06bdee9c63ad829de2432635be6d0ce69"
|
||||||
|
integrity sha512-USzVzLGrvZ8VK1/sEsOAmeqa8N7D3OBdZskVaL7DL89Q4QLTYD053iIlZ5KDidyZ+Od80Dttin/f8ZulOLFFDQ==
|
||||||
|
|
||||||
zwitch@^1.0.0:
|
zwitch@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||||
|
Loading…
Reference in New Issue
Block a user