Merge pull request #376 from mikecao/dev

v1.6.0
This commit is contained in:
Mike Cao 2020-11-17 22:30:59 -08:00 committed by GitHub
commit d250f3c678
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 494 additions and 359 deletions

View File

@ -8,10 +8,12 @@ import { THEME_COLORS } from 'lib/constants';
import styles from './WorldMap.module.css'; import styles from './WorldMap.module.css';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import { useRouter } from 'next/router';
const geoUrl = '/world-110m.json'; const geoUrl = '/world-110m.json';
export default function WorldMap({ data, className }) { export default function WorldMap({ data, className }) {
const { basePath } = useRouter();
const [tooltip, setTooltip] = useState(); const [tooltip, setTooltip] = useState();
const [theme] = useTheme(); const [theme] = useTheme();
const colors = useMemo( const colors = useMemo(
@ -57,7 +59,7 @@ export default function WorldMap({ data, className }) {
> >
<ComposableMap projection="geoMercator"> <ComposableMap projection="geoMercator">
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}> <ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
<Geographies geography={geoUrl}> <Geographies geography={`${basePath}${geoUrl}`}>
{({ geographies }) => { {({ geographies }) => {
return geographies.map(geo => { return geographies.map(geo => {
const code = geo.properties.ISO_A2; const code = geo.properties.ISO_A2;

View File

@ -1,8 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik'; import { Formik, Form, Field } from 'formik';
import { useRouter } from 'next/router';
import { post } from 'lib/web';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import FormLayout, { import FormLayout, {
FormButtons, FormButtons,
@ -10,6 +8,7 @@ import FormLayout, {
FormMessage, FormMessage,
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import usePost from 'hooks/usePost';
const initialValues = { const initialValues = {
username: '', username: '',
@ -30,11 +29,11 @@ const validate = ({ user_id, username, password }) => {
}; };
export default function AccountEditForm({ values, onSave, onClose }) { export default function AccountEditForm({ values, onSave, onClose }) {
const { basePath } = useRouter(); const post = usePost();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async values => { const handleSubmit = async values => {
const { ok, data } = await post(`${basePath}/api/account`, values); const { ok, data } = await post('/api/account', values);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -1,8 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import { Formik, Form, Field } from 'formik'; import { Formik, Form, Field } from 'formik';
import { post } from 'lib/web';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import FormLayout, { import FormLayout, {
FormButtons, FormButtons,
@ -10,6 +8,7 @@ import FormLayout, {
FormMessage, FormMessage,
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import usePost from 'hooks/usePost';
const initialValues = { const initialValues = {
current_password: '', current_password: '',
@ -38,11 +37,11 @@ const validate = ({ current_password, new_password, confirm_password }) => {
}; };
export default function ChangePasswordForm({ values, onSave, onClose }) { export default function ChangePasswordForm({ values, onSave, onClose }) {
const { basePath } = useRouter(); const post = usePost();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async values => { const handleSubmit = async values => {
const { ok, data } = await post(`${basePath}/api/account/password`, values); const { ok, data } = await post('/api/account/password', values);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -1,8 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import { Formik, Form, Field } from 'formik'; import { Formik, Form, Field } from 'formik';
import { del } from 'lib/web';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import FormLayout, { import FormLayout, {
FormButtons, FormButtons,
@ -10,6 +8,7 @@ import FormLayout, {
FormMessage, FormMessage,
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import useDelete from 'hooks/useDelete';
const CONFIRMATION_WORD = 'DELETE'; const CONFIRMATION_WORD = 'DELETE';
@ -28,11 +27,11 @@ const validate = ({ confirmation }) => {
}; };
export default function DeleteForm({ values, onSave, onClose }) { export default function DeleteForm({ values, onSave, onClose }) {
const { basePath } = useRouter(); const del = useDelete();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async ({ type, id }) => { const handleSubmit = async ({ type, id }) => {
const { ok, data } = await del(`${basePath}/api/${type}/${id}`); const { ok, data } = await del(`/api/${type}/${id}`);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik'; import { Formik, Form, Field } from 'formik';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { post } from 'lib/web';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import FormLayout, { import FormLayout, {
FormButtons, FormButtons,
@ -13,6 +12,7 @@ import FormLayout, {
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
import styles from './LoginForm.module.css'; import styles from './LoginForm.module.css';
import usePost from 'hooks/usePost';
const validate = ({ username, password }) => { const validate = ({ username, password }) => {
const errors = {}; const errors = {};
@ -28,11 +28,12 @@ const validate = ({ username, password }) => {
}; };
export default function LoginForm() { export default function LoginForm() {
const post = usePost();
const router = useRouter(); const router = useRouter();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async ({ username, password }) => { const handleSubmit = async ({ username, password }) => {
const { ok, status, data } = await post(`${router.basePath}/api/auth/login`, { const { ok, status, data } = await post('/api/auth/login', {
username, username,
password, password,
}); });
@ -65,8 +66,10 @@ export default function LoginForm() {
> >
{() => ( {() => (
<Form> <Form>
<div className={styles.header}>
<Icon icon={<Logo />} size="xlarge" className={styles.icon} /> <Icon icon={<Logo />} size="xlarge" className={styles.icon} />
<h1 className="center">umami</h1> <h1 className="center">umami</h1>
</div>
<FormRow> <FormRow>
<label htmlFor="username"> <label htmlFor="username">
<FormattedMessage id="label.username" defaultMessage="Username" /> <FormattedMessage id="label.username" defaultMessage="Username" />

View File

@ -13,3 +13,11 @@
justify-content: center; justify-content: center;
margin: 0 auto; margin: 0 auto;
} }
.header {
margin-bottom: 30px;
}
.header h1 {
margin: 12px 0;
}

View File

@ -1,7 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik'; import { Formik, Form, Field } from 'formik';
import { post } from 'lib/web';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import FormLayout, { import FormLayout, {
FormButtons, FormButtons,
@ -11,7 +10,7 @@ import FormLayout, {
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import Checkbox from 'components/common/Checkbox'; import Checkbox from 'components/common/Checkbox';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { useRouter } from 'next/router'; import usePost from 'hooks/usePost';
const initialValues = { const initialValues = {
name: '', name: '',
@ -35,11 +34,11 @@ const validate = ({ name, domain }) => {
}; };
export default function WebsiteEditForm({ values, onSave, onClose }) { export default function WebsiteEditForm({ values, onSave, onClose }) {
const { basePath } = useRouter(); const post = usePost();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async values => { const handleSubmit = async values => {
const { ok, data } = await post(`${basePath}/api/website`, values); const { ok, data } = await post('/api/website', values);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -60,7 +60,7 @@ export default function MetricsBar({ websiteId, className }) {
/> />
<MetricCard <MetricCard
label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />} label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />}
value={pageviews ? (bounces / pageviews) * 100 : 0} value={uniques ? (bounces / uniques) * 100 : 0}
format={n => Number(n).toFixed(0) + '%'} format={n => Number(n).toFixed(0) + '%'}
/> />
<MetricCard <MetricCard

View File

@ -43,19 +43,12 @@ export default function WebsiteDetails({ websiteId }) {
const [eventsData, setEventsData] = useState(); const [eventsData, setEventsData] = useState();
const { const {
resolve, resolve,
router,
query: { view }, query: { view },
} = usePageQuery(); } = usePageQuery();
const BackButton = () => ( const BackButton = () => (
<div key="back-button" className={styles.backButton}> <div key="back-button" className={styles.backButton}>
<Link <Link key="back-button" href={resolve({ view: undefined })} icon={<Arrow />} size="small">
key="back-button"
href={router.pathname}
as={resolve({ view: undefined })}
icon={<Arrow />}
size="small"
>
<FormattedMessage id="label.back" defaultMessage="Back" /> <FormattedMessage id="label.back" defaultMessage="Back" />
</Link> </Link>
</div> </div>

11
hooks/useDelete.js Normal file
View File

@ -0,0 +1,11 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { del } from 'lib/web';
export default function useDelete() {
const { basePath } = useRouter();
return useCallback(async (url, params, headers) => {
return del(`${basePath}${url}`, params, headers);
}, []);
}

View File

@ -6,8 +6,7 @@ import { useRouter } from 'next/router';
export default function useFetch(url, options = {}, update = []) { export default function useFetch(url, options = {}, update = []) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [data, setData] = useState(); const [response, setResponse] = useState();
const [status, setStatus] = useState();
const [error, setError] = useState(); const [error, setError] = useState();
const [loading, setLoadiing] = useState(false); const [loading, setLoadiing] = useState(false);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
@ -19,18 +18,17 @@ export default function useFetch(url, options = {}, update = []) {
setLoadiing(true); setLoadiing(true);
setError(null); setError(null);
const time = performance.now(); const time = performance.now();
const { data, status } = 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() })); dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
if (status >= 400) { if (status >= 400) {
setError(data); setError(data);
setData(null); setResponse({ data: null, status, ok });
} else { } else {
setData(data); setResponse({ data, status, ok });
} }
setStatus(status);
onDataLoad?.(data); onDataLoad?.(data);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -60,5 +58,5 @@ export default function useFetch(url, options = {}, update = []) {
} }
}, [interval, !!disabled]); }, [interval, !!disabled]);
return { data, status, error, loading }; return { ...response, error, loading };
} }

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getQueryString } from '../lib/url'; import { getQueryString } from 'lib/url';
export default function usePageQuery() { export default function usePageQuery() {
const router = useRouter(); const router = useRouter();
@ -25,7 +25,9 @@ export default function usePageQuery() {
function resolve(params) { function resolve(params) {
const search = getQueryString({ ...query, ...params }); const search = getQueryString({ ...query, ...params });
return `${pathname}${search}`; const { asPath } = router;
return `${asPath.split('?')[0]}${search}`;
} }
return { pathname, query, resolve, router }; return { pathname, query, resolve, router };

11
hooks/usePost.js Normal file
View File

@ -0,0 +1,11 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { post } from 'lib/web';
export default function usePost() {
const { basePath } = useRouter();
return useCallback(async (url, params, headers) => {
return post(`${basePath}${url}`, params, headers);
}, []);
}

View File

@ -2,17 +2,7 @@ import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { updateUser } from 'redux/actions/user'; import { updateUser } from 'redux/actions/user';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { get } from '../lib/web'; import { get } from 'lib/web';
export async function fetchUser() {
const res = await fetch('/api/auth/verify');
if (!res.ok) {
return null;
}
return await res.json();
}
export default function useRequireLogin() { export default function useRequireLogin() {
const router = useRouter(); const router = useRouter();

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "1.5.0", "version": "1.6.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ", "description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -56,7 +56,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@prisma/client": "2.10.1", "@prisma/client": "2.11.0",
"@reduxjs/toolkit": "^1.4.0", "@reduxjs/toolkit": "^1.4.0",
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
@ -75,16 +75,16 @@
"jose": "^2.0.3", "jose": "^2.0.3",
"maxmind": "^4.3.1", "maxmind": "^4.3.1",
"moment-timezone": "^0.5.31", "moment-timezone": "^0.5.31",
"next": "^10.0.0", "next": "^10.0.1",
"prompts": "2.4.0", "prompts": "2.4.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-intl": "^5.8.8", "react-intl": "^5.10.1",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-simple-maps": "^2.3.0", "react-simple-maps": "^2.3.0",
"react-spring": "^8.0.27", "react-spring": "^8.0.27",
"react-tooltip": "^4.2.10", "react-tooltip": "^4.2.10",
"react-use-measure": "^2.0.2", "react-use-measure": "^2.0.3",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
@ -97,7 +97,7 @@
}, },
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^2.13.5", "@formatjs/cli": "^2.13.5",
"@prisma/cli": "2.10.1", "@prisma/cli": "2.11.0",
"@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^10.0.0", "@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-replace": "^2.3.4",
@ -105,7 +105,7 @@
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"del": "^6.0.0", "del": "^6.0.0",
"dotenv-cli": "^4.0.0", "dotenv-cli": "^4.0.0",
"eslint": "^7.12.1", "eslint": "^7.13.0",
"eslint-config-prettier": "^6.15.0", "eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.21.5",
@ -120,10 +120,10 @@
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"prettier-eslint": "^11.0.0", "prettier-eslint": "^11.0.0",
"rollup": "^2.33.0", "rollup": "^2.33.2",
"rollup-plugin-hashbang": "^2.2.2", "rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"stylelint": "^13.7.2", "stylelint": "^13.8.0",
"stylelint-config-css-modules": "^2.2.0", "stylelint-config-css-modules": "^2.2.0",
"stylelint-config-prettier": "^8.0.1", "stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0", "stylelint-config-recommended": "^3.0.0",

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import Head from "next/head";
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { useStore } from 'redux/store'; import { useStore } from 'redux/store';
@ -9,6 +10,7 @@ import 'styles/variables.css';
import 'styles/bootstrap-grid.css'; import 'styles/bootstrap-grid.css';
import 'styles/index.css'; import 'styles/index.css';
const Intl = ({ children }) => { const Intl = ({ children }) => {
const [locale] = useLocale(); const [locale] = useLocale();
@ -27,6 +29,15 @@ export default function App({ Component, pageProps }) {
return ( return (
<Provider store={store}> <Provider store={store}>
<Head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
</Head>
<Intl> <Intl>
<Component {...pageProps} /> <Component {...pageProps} />
</Intl> </Intl>

View File

@ -1,12 +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 { updateUser } from 'redux/actions/user';
export default function LogoutPage() { export default function LogoutPage() {
const dispatch = useDispatch();
const router = useRouter(); const router = useRouter();
const { basePath } = router; const { basePath } = router;
useEffect(() => { useEffect(() => {
fetch(`${basePath}/api/auth/logout`).then(() => router.push('/login')); dispatch(updateUser(null));
get(`${basePath}/api/auth/logout`).then(() => router.push('/login'));
}, []); }, []);
return null; return null;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

9
public/browserconfig.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,75 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="856.000000pt" height="856.000000pt" viewBox="0 0 856.000000 856.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,856.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M4027 8163 c-2 -2 -28 -5 -58 -7 -50 -4 -94 -9 -179 -22 -19 -2 -48
-6 -65 -9 -47 -6 -236 -44 -280 -55 -22 -6 -49 -12 -60 -15 -34 -6 -58 -13
-130 -36 -38 -13 -72 -23 -75 -24 -29 -6 -194 -66 -264 -96 -49 -22 -95 -39
-102 -39 -7 0 -19 -7 -28 -15 -8 -9 -18 -15 -21 -14 -7 1 -197 -92 -205 -101
-3 -3 -21 -13 -40 -24 -79 -42 -123 -69 -226 -137 -94 -62 -246 -173 -280
-204 -6 -5 -29 -25 -52 -43 -136 -111 -329 -305 -457 -462 -21 -25 -41 -47
-44 -50 -4 -3 -22 -26 -39 -52 -18 -25 -38 -52 -45 -60 -34 -35 -207 -308
-259 -408 -13 -25 -25 -47 -28 -50 -11 -11 -121 -250 -159 -346 -42 -105 -114
-321 -126 -374 l-7 -30 -263 0 c-245 0 -268 -2 -321 -21 -94 -35 -171 -122
-191 -216 -9 -39 -8 -852 0 -938 9 -87 16 -150 23 -195 3 -19 6 -48 8 -65 3
-29 14 -97 22 -140 3 -11 7 -36 10 -55 3 -19 9 -51 14 -70 5 -19 11 -46 14
-60 29 -138 104 -401 145 -505 5 -11 23 -58 42 -105 18 -47 42 -105 52 -130
11 -25 21 -49 22 -55 3 -10 109 -224 164 -330 18 -33 50 -89 71 -124 22 -34
40 -64 40 -66 0 -8 104 -161 114 -167 6 -4 7 -8 3 -8 -4 0 4 -12 18 -27 14
-15 25 -32 25 -36 0 -5 6 -14 13 -21 6 -7 21 -25 32 -41 11 -15 34 -44 50 -64
17 -21 41 -52 55 -70 13 -18 33 -43 45 -56 11 -13 42 -49 70 -81 100 -118 359
-369 483 -469 34 -27 62 -53 62 -57 0 -5 6 -8 13 -8 7 0 19 -9 27 -20 8 -11
19 -20 26 -20 6 0 19 -9 29 -20 10 -11 22 -20 27 -20 5 0 23 -13 41 -30 18
-16 37 -30 44 -30 6 0 13 -4 15 -8 3 -8 186 -132 194 -132 2 0 27 -15 56 -34
132 -83 377 -207 558 -280 36 -15 74 -31 85 -36 62 -26 220 -81 320 -109 79
-23 191 -53 214 -57 14 -3 28 -7 31 -9 4 -2 20 -7 36 -9 16 -3 40 -8 54 -11
14 -3 36 -8 50 -11 14 -2 36 -7 50 -10 13 -3 40 -8 60 -10 19 -2 46 -7 60 -10
54 -10 171 -25 320 -40 90 -9 613 -12 636 -4 11 5 28 4 37 -1 9 -6 17 -6 17
-1 0 4 10 8 23 9 29 0 154 12 192 18 17 3 46 7 65 9 70 10 131 20 183 32 16 3
38 7 50 9 45 7 165 36 252 60 50 14 100 28 112 30 12 3 34 10 48 15 14 5 25 7
25 4 0 -4 6 -2 13 3 6 6 30 16 52 22 22 7 47 15 55 18 8 4 17 7 20 7 10 2 179
68 240 94 96 40 342 159 395 191 17 10 53 30 80 46 28 15 81 47 118 71 37 24
72 44 76 44 5 0 11 3 13 8 2 4 30 25 63 47 33 22 62 42 65 45 3 3 50 38 105
79 55 40 105 79 110 85 6 6 24 22 40 34 85 65 465 430 465 447 0 3 8 13 18 23
9 10 35 40 57 66 22 27 47 56 55 65 8 9 42 52 74 96 32 44 71 96 85 115 140
183 358 576 461 830 12 30 28 69 36 85 24 56 123 355 117 355 -3 0 -1 6 5 13
6 6 14 30 18 52 10 48 9 46 17 65 5 13 37 155 52 230 9 42 35 195 40 231 34
235 40 357 40 804 l0 420 -24 44 c-46 87 -143 157 -231 166 -19 2 -144 4 -276
4 l-242 1 -36 118 c-21 64 -46 139 -56 166 -11 27 -20 52 -20 57 0 5 -11 33
-25 63 -14 30 -25 58 -25 61 0 18 -152 329 -162 333 -5 2 -8 10 -8 18 0 8 -4
14 -10 14 -5 0 -9 3 -8 8 3 9 -40 82 -128 217 -63 97 -98 145 -187 259 -133
171 -380 420 -559 564 -71 56 -132 102 -138 102 -5 0 -10 3 -10 8 0 4 -25 23
-55 42 -30 19 -55 38 -55 43 0 4 -6 7 -13 7 -7 0 -22 8 -33 18 -11 9 -37 26
-59 37 -21 11 -44 25 -50 30 -41 37 -413 220 -540 266 -27 9 -61 22 -75 27
-14 5 -28 10 -32 11 -4 1 -28 10 -53 21 -25 11 -46 19 -48 18 -2 -1 -109 29
-137 40 -13 4 -32 9 -65 16 -5 1 -16 5 -22 9 -7 5 -13 6 -13 3 0 -2 -15 0 -32
5 -18 5 -44 11 -58 14 -14 3 -36 7 -50 10 -14 3 -50 9 -80 15 -30 6 -64 12
-75 14 -11 2 -45 6 -75 10 -30 4 -71 9 -90 12 -19 3 -53 6 -75 7 -22 1 -44 5
-50 8 -11 7 -542 9 -548 2z m57 -404 c7 10 436 8 511 -3 22 -3 60 -8 85 -11
25 -2 56 -6 70 -9 14 -2 43 -7 65 -10 38 -5 58 -9 115 -21 14 -3 34 -7 45 -9
11 -2 58 -14 105 -26 47 -12 92 -23 100 -25 35 -7 279 -94 308 -109 17 -9 34
-16 37 -16 3 1 20 -6 38 -14 17 -8 68 -31 112 -51 44 -20 82 -35 84 -35 2 1 7
-3 10 -8 3 -5 43 -28 88 -51 45 -23 87 -48 93 -56 7 -8 17 -15 22 -15 12 0
192 -121 196 -132 2 -4 8 -8 13 -8 10 0 119 -86 220 -172 102 -87 256 -244
349 -357 25 -30 53 -63 63 -73 9 -10 17 -22 17 -28 0 -5 3 -10 8 -10 4 0 25
-27 46 -60 22 -33 43 -60 48 -60 4 0 8 -5 8 -11 0 -6 11 -25 25 -43 14 -18 25
-38 25 -44 0 -7 4 -12 8 -12 5 0 16 -15 25 -32 9 -18 30 -55 47 -83 46 -77
161 -305 154 -305 -4 0 -2 -6 4 -12 6 -7 23 -47 40 -88 16 -41 33 -84 37 -95
5 -11 9 -22 10 -25 0 -3 11 -36 24 -73 13 -38 21 -70 19 -73 -3 -2 -1386 -3
-3075 -2 l-3071 3 38 110 c47 137 117 301 182 425 62 118 167 295 191 320 9
11 17 22 17 25 0 7 39 63 58 83 6 7 26 35 44 60 18 26 37 52 43 57 6 6 34 37
61 70 48 59 271 286 329 335 17 14 53 43 80 65 28 22 52 42 55 45 3 3 21 17
40 30 19 14 40 28 45 32 40 32 105 78 109 78 3 0 28 16 55 35 26 19 53 35 58
35 5 0 18 8 29 18 17 15 53 35 216 119 118 60 412 176 422 166 3 -4 6 -2 6 4
0 6 12 13 28 16 15 3 52 12 82 21 30 9 63 19 73 21 10 2 27 7 37 10 10 3 29 8
42 10 13 3 48 10 78 16 30 7 61 12 68 12 6 0 12 4 12 9 0 5 5 6 10 3 6 -4 34
-2 63 4 51 11 71 13 197 26 36 4 67 9 69 11 2 2 10 -1 17 -7 8 -6 14 -7 18 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

19
public/site.webmanifest Normal file
View File

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

600
yarn.lock

File diff suppressed because it is too large Load Diff