commit
4047a7ec23
378 changed files with 29334 additions and 0 deletions
34
hooks/useCountryNames.js
Normal file
34
hooks/useCountryNames.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { get } from 'lib/web';
|
||||
import enUS from 'public/country/en-US.json';
|
||||
|
||||
const countryNames = {
|
||||
'en-US': enUS,
|
||||
};
|
||||
|
||||
export default function useCountryNames(locale) {
|
||||
const [list, setList] = useState(countryNames[locale] || enUS);
|
||||
const { basePath } = useRouter();
|
||||
|
||||
async function loadData(locale) {
|
||||
const { ok, data } = await get(`${basePath}/country/${locale}.json`);
|
||||
|
||||
if (ok) {
|
||||
countryNames[locale] = data;
|
||||
setList(countryNames[locale]);
|
||||
} else {
|
||||
setList(enUS);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!countryNames[locale]) {
|
||||
loadData(locale);
|
||||
} else {
|
||||
setList(countryNames[locale]);
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
return list;
|
||||
}
|
||||
43
hooks/useDateRange.js
Normal file
43
hooks/useDateRange.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import { getItem, setItem } from 'lib/web';
|
||||
import { setDateRange } from '../redux/actions/websites';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import useForceUpdate from './useForceUpdate';
|
||||
import useLocale from './useLocale';
|
||||
|
||||
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
|
||||
const dispatch = useDispatch();
|
||||
const { locale } = useLocale();
|
||||
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const globalDefault = getItem(DATE_RANGE_CONFIG);
|
||||
let globalDateRange;
|
||||
|
||||
if (globalDefault) {
|
||||
if (typeof globalDefault === 'string') {
|
||||
globalDateRange = getDateRange(globalDefault, locale);
|
||||
} else if (typeof globalDefault === 'object') {
|
||||
globalDateRange = {
|
||||
...globalDefault,
|
||||
startDate: parseISO(globalDefault.startDate),
|
||||
endDate: parseISO(globalDefault.endDate),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function saveDateRange(values) {
|
||||
const { value } = values;
|
||||
|
||||
if (websiteId) {
|
||||
dispatch(setDateRange(websiteId, values));
|
||||
} else {
|
||||
setItem(DATE_RANGE_CONFIG, value === 'custom' ? values : value);
|
||||
forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
return [dateRange || globalDateRange || getDateRange(defaultDateRange, locale), saveDateRange];
|
||||
}
|
||||
11
hooks/useDelete.js
Normal file
11
hooks/useDelete.js
Normal 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);
|
||||
}, []);
|
||||
}
|
||||
13
hooks/useDocumentClick.js
Normal file
13
hooks/useDocumentClick.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export default function useDocumentClick(handler) {
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handler);
|
||||
};
|
||||
}, [handler]);
|
||||
|
||||
return null;
|
||||
}
|
||||
19
hooks/useEscapeKey.js
Normal file
19
hooks/useEscapeKey.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
export default function useEscapeKey(handler) {
|
||||
const escFunction = useCallback(event => {
|
||||
if (event.keyCode === 27) {
|
||||
handler(event);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', escFunction, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escFunction, false);
|
||||
};
|
||||
}, [escFunction]);
|
||||
|
||||
return null;
|
||||
}
|
||||
62
hooks/useFetch.js
Normal file
62
hooks/useFetch.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get } from 'lib/web';
|
||||
import { updateQuery } from 'redux/actions/queries';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function useFetch(url, options = {}, update = []) {
|
||||
const dispatch = useDispatch();
|
||||
const [response, setResponse] = useState();
|
||||
const [error, setError] = useState();
|
||||
const [loading, setLoadiing] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
const { basePath } = useRouter();
|
||||
const { params = {}, disabled, headers, delay = 0, interval, onDataLoad } = options;
|
||||
|
||||
async function loadData(params) {
|
||||
try {
|
||||
setLoadiing(true);
|
||||
setError(null);
|
||||
const time = performance.now();
|
||||
const { data, status, ok } = await get(`${basePath}${url}`, params, headers);
|
||||
|
||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
||||
|
||||
if (status >= 400) {
|
||||
setError(data);
|
||||
setResponse({ data: null, status, ok });
|
||||
} else {
|
||||
setResponse({ data, status, ok });
|
||||
}
|
||||
|
||||
onDataLoad?.(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
} finally {
|
||||
setLoadiing(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (url && !disabled) {
|
||||
const id = setTimeout(() => loadData(params), delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
}, [url, !!disabled, count, ...update]);
|
||||
|
||||
useEffect(() => {
|
||||
if (interval && !disabled) {
|
||||
const id = setInterval(() => setCount(state => state + 1), interval);
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
}
|
||||
}, [interval, !!disabled]);
|
||||
|
||||
return { ...response, error, loading };
|
||||
}
|
||||
14
hooks/useForceSSL.js
Normal file
14
hooks/useForceSSL.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function useForceSSL(enabled) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && typeof window !== 'undefined' && /^http:\/\//.test(location.href)) {
|
||||
router.push(location.href.replace(/^http:\/\//, 'https://'));
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
return null;
|
||||
}
|
||||
9
hooks/useForceUpdate.js
Normal file
9
hooks/useForceUpdate.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
export default function useForceUpdate() {
|
||||
const [, update] = useState(Object.create(null));
|
||||
|
||||
return useCallback(() => {
|
||||
update(Object.create(null));
|
||||
}, [update]);
|
||||
}
|
||||
52
hooks/useLocale.js
Normal file
52
hooks/useLocale.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setLocale } from 'redux/actions/app';
|
||||
import { useRouter } from 'next/router';
|
||||
import { get, setItem } from 'lib/web';
|
||||
import { LOCALE_CONFIG } from 'lib/constants';
|
||||
import { getDateLocale, getTextDirection } from 'lib/lang';
|
||||
import useForceUpdate from 'hooks/useForceUpdate';
|
||||
import enUS from 'public/lang/en-US.json';
|
||||
|
||||
const messages = {
|
||||
'en-US': enUS,
|
||||
};
|
||||
|
||||
export default function useLocale() {
|
||||
const locale = useSelector(state => state.app.locale);
|
||||
const dispatch = useDispatch();
|
||||
const { basePath } = useRouter();
|
||||
const forceUpdate = useForceUpdate();
|
||||
const dir = getTextDirection(locale);
|
||||
const dateLocale = getDateLocale(locale);
|
||||
|
||||
async function loadMessages(locale) {
|
||||
const { ok, data } = await get(`${basePath}/lang/${locale}.json`);
|
||||
|
||||
if (ok) {
|
||||
messages[locale] = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLocale(value) {
|
||||
if (!messages[value]) {
|
||||
await loadMessages(value);
|
||||
}
|
||||
|
||||
setItem(LOCALE_CONFIG, value);
|
||||
|
||||
if (locale !== value) {
|
||||
dispatch(setLocale(value));
|
||||
} else {
|
||||
forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!messages[locale]) {
|
||||
saveLocale(locale);
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
return { locale, saveLocale, messages, dir, dateLocale };
|
||||
}
|
||||
34
hooks/usePageQuery.js
Normal file
34
hooks/usePageQuery.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getQueryString } from 'lib/url';
|
||||
|
||||
export default function usePageQuery() {
|
||||
const router = useRouter();
|
||||
const { pathname, search } = location;
|
||||
|
||||
const query = useMemo(() => {
|
||||
if (!search) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const params = search.substring(1).split('&');
|
||||
|
||||
return params.reduce((obj, item) => {
|
||||
const [key, value] = item.split('=');
|
||||
|
||||
obj[key] = decodeURIComponent(value);
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}, [search]);
|
||||
|
||||
function resolve(params) {
|
||||
const search = getQueryString({ ...query, ...params });
|
||||
|
||||
const { asPath } = router;
|
||||
|
||||
return `${asPath.split('?')[0]}${search}`;
|
||||
}
|
||||
|
||||
return { pathname, query, resolve, router };
|
||||
}
|
||||
11
hooks/usePost.js
Normal file
11
hooks/usePost.js
Normal 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);
|
||||
}, []);
|
||||
}
|
||||
38
hooks/useRequireLogin.js
Normal file
38
hooks/useRequireLogin.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateUser } from 'redux/actions/user';
|
||||
import { useRouter } from 'next/router';
|
||||
import { get } from 'lib/web';
|
||||
|
||||
export default function useRequireLogin() {
|
||||
const router = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
const storeUser = useSelector(state => state.user);
|
||||
const [loading, setLoading] = useState(!storeUser);
|
||||
const [user, setUser] = useState(storeUser);
|
||||
|
||||
async function loadUser() {
|
||||
setLoading(true);
|
||||
|
||||
const { ok, data } = await get(`${router.basePath}/api/auth/verify`);
|
||||
|
||||
if (!ok) {
|
||||
return router.push('/login');
|
||||
}
|
||||
|
||||
await dispatch(updateUser(data));
|
||||
|
||||
setUser(user);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
return { user, loading };
|
||||
}
|
||||
27
hooks/useShareToken.js
Normal file
27
hooks/useShareToken.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouter } from 'next/router';
|
||||
import { get } from 'lib/web';
|
||||
import { setShareToken } from 'redux/actions/app';
|
||||
|
||||
export default function useShareToken(shareId) {
|
||||
const { basePath } = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
const shareToken = useSelector(state => state.app.shareToken);
|
||||
|
||||
async function loadToken(id) {
|
||||
const { data } = await get(`${basePath}/api/share/${id}`);
|
||||
|
||||
if (data) {
|
||||
dispatch(setShareToken(data));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (shareId) {
|
||||
loadToken(shareId);
|
||||
}
|
||||
}, [shareId]);
|
||||
|
||||
return shareToken;
|
||||
}
|
||||
27
hooks/useTheme.js
Normal file
27
hooks/useTheme.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setTheme } from 'redux/actions/app';
|
||||
import { getItem, setItem } from 'lib/web';
|
||||
import { THEME_CONFIG } from 'lib/constants';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function useTheme() {
|
||||
const defaultTheme =
|
||||
typeof window !== 'undefined'
|
||||
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
: 'light';
|
||||
const theme = useSelector(state => state.app.theme || getItem(THEME_CONFIG) || defaultTheme);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
function saveTheme(value) {
|
||||
setItem(THEME_CONFIG, value);
|
||||
dispatch(setTheme(value));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('data-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
return [theme, saveTheme];
|
||||
}
|
||||
18
hooks/useTimezone.js
Normal file
18
hooks/useTimezone.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { getTimezone } from 'lib/date';
|
||||
import { getItem, setItem } from 'lib/web';
|
||||
import { TIMEZONE_CONFIG } from 'lib/constants';
|
||||
|
||||
export default function useTimezone() {
|
||||
const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone());
|
||||
|
||||
const saveTimezone = useCallback(
|
||||
value => {
|
||||
setItem(TIMEZONE_CONFIG, value);
|
||||
setTimezone(value);
|
||||
},
|
||||
[setTimezone],
|
||||
);
|
||||
|
||||
return [timezone, saveTimezone];
|
||||
}
|
||||
23
hooks/useVersion.js
Normal file
23
hooks/useVersion.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { useEffect, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { checkVersion } from 'redux/actions/app';
|
||||
import { VERSION_CHECK } from 'lib/constants';
|
||||
import { getItem, setItem } from 'lib/web';
|
||||
|
||||
export default function useVersion(check) {
|
||||
const dispatch = useDispatch();
|
||||
const versions = useSelector(state => state.app.versions);
|
||||
const checked = versions.latest === getItem(VERSION_CHECK)?.version;
|
||||
|
||||
const updateCheck = useCallback(() => {
|
||||
setItem(VERSION_CHECK, { version: versions.latest, time: Date.now() });
|
||||
}, [versions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (check && !versions.latest) {
|
||||
dispatch(checkVersion());
|
||||
}
|
||||
}, [versions, check]);
|
||||
|
||||
return { ...versions, checked, updateCheck };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue