mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 21:27:20 +01:00
Moved code into src folder. Added build for component library.
This commit is contained in:
parent
7a7233ead4
commit
ede658771e
490 changed files with 749 additions and 442 deletions
23
src/components/hooks/index.js
Normal file
23
src/components/hooks/index.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export * from './useApi';
|
||||
export * from './useConfig';
|
||||
export * from './useCountryNames';
|
||||
export * from './useDateRange';
|
||||
export * from './useDocumentClick';
|
||||
export * from './useEscapeKey';
|
||||
export * from './useFilters';
|
||||
export * from './useForceUpdate';
|
||||
export * from './useFormat';
|
||||
export * from './useLanguageNames';
|
||||
export * from './useLocale';
|
||||
export * from './useMessages';
|
||||
export * from './usePageQuery';
|
||||
export * from './useReport';
|
||||
export * from './useReports';
|
||||
export * from './useRequireLogin';
|
||||
export * from './useShareToken';
|
||||
export * from './useSticky';
|
||||
export * from './useTheme';
|
||||
export * from './useTimezone';
|
||||
export * from './useUser';
|
||||
export * from './useWebsite';
|
||||
export * from './useWebsiteReports';
|
||||
22
src/components/hooks/useApi.ts
Normal file
22
src/components/hooks/useApi.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import * as reactQuery from '@tanstack/react-query';
|
||||
import { useApi as nextUseApi } from 'next-basics';
|
||||
import { getClientAuthToken } from 'lib/client';
|
||||
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||
import useStore from 'store/app';
|
||||
|
||||
const selector = state => state.shareToken;
|
||||
|
||||
export function useApi() {
|
||||
const { basePath } = useRouter();
|
||||
const shareToken = useStore(selector);
|
||||
|
||||
const { get, post, put, del } = nextUseApi(
|
||||
{ authorization: `Bearer ${getClientAuthToken()}`, [SHARE_TOKEN_HEADER]: shareToken?.token },
|
||||
basePath,
|
||||
);
|
||||
|
||||
return { get, post, put, del, ...reactQuery };
|
||||
}
|
||||
|
||||
export default useApi;
|
||||
28
src/components/hooks/useApiFilter.ts
Normal file
28
src/components/hooks/useApiFilter.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
export function useApiFilter() {
|
||||
const [filter, setFilter] = useState();
|
||||
const [filterType, setFilterType] = useState('All');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const handleFilterChange = value => setFilter(value);
|
||||
const handlePageChange = value => setPage(value);
|
||||
const handlePageSizeChange = value => setPageSize(value);
|
||||
|
||||
return {
|
||||
filter,
|
||||
setFilter,
|
||||
filterType,
|
||||
setFilterType,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default useApiFilter;
|
||||
27
src/components/hooks/useConfig.js
Normal file
27
src/components/hooks/useConfig.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect } from 'react';
|
||||
import useStore, { setConfig } from 'store/app';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
|
||||
let loading = false;
|
||||
|
||||
export function useConfig() {
|
||||
const { config } = useStore();
|
||||
const { get } = useApi();
|
||||
|
||||
async function loadConfig() {
|
||||
const data = await get('/config');
|
||||
loading = false;
|
||||
setConfig(data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!config && !loading) {
|
||||
loading = true;
|
||||
loadConfig();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export default useConfig;
|
||||
36
src/components/hooks/useCountryNames.js
Normal file
36
src/components/hooks/useCountryNames.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { httpGet } from 'next-basics';
|
||||
import enUS from 'public/intl/country/en-US.json';
|
||||
|
||||
const countryNames = {
|
||||
'en-US': enUS,
|
||||
};
|
||||
|
||||
export function useCountryNames(locale) {
|
||||
const [list, setList] = useState(countryNames[locale] || enUS);
|
||||
const { basePath } = useRouter();
|
||||
|
||||
async function loadData(locale) {
|
||||
const { data } = await httpGet(`${basePath}/intl/country/${locale}.json`);
|
||||
|
||||
if (data) {
|
||||
countryNames[locale] = data;
|
||||
setList(countryNames[locale]);
|
||||
} else {
|
||||
setList(enUS);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!countryNames[locale]) {
|
||||
loadData(locale);
|
||||
} else {
|
||||
setList(countryNames[locale]);
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
export default useCountryNames;
|
||||
50
src/components/hooks/useDateRange.js
Normal file
50
src/components/hooks/useDateRange.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { getMinimumUnit, parseDateRange } from 'lib/date';
|
||||
import { setItem } from 'next-basics';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import useLocale from './useLocale';
|
||||
import websiteStore, { setWebsiteDateRange } from 'store/websites';
|
||||
import appStore, { setDateRange } from 'store/app';
|
||||
import useApi from './useApi';
|
||||
|
||||
export function useDateRange(websiteId) {
|
||||
const { get } = useApi();
|
||||
const { locale } = useLocale();
|
||||
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
|
||||
const defaultConfig = DEFAULT_DATE_RANGE;
|
||||
const globalConfig = appStore(state => state.dateRange);
|
||||
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
|
||||
|
||||
const saveDateRange = async value => {
|
||||
if (websiteId) {
|
||||
let dateRange = value;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (value === 'all') {
|
||||
const result = await get(`/websites/${websiteId}/daterange`);
|
||||
const { mindate, maxdate } = result;
|
||||
|
||||
const startDate = new Date(mindate);
|
||||
const endDate = new Date(maxdate);
|
||||
|
||||
dateRange = {
|
||||
startDate,
|
||||
endDate,
|
||||
unit: getMinimumUnit(startDate, endDate),
|
||||
value,
|
||||
};
|
||||
} else {
|
||||
dateRange = parseDateRange(value, locale);
|
||||
}
|
||||
}
|
||||
|
||||
setWebsiteDateRange(websiteId, dateRange);
|
||||
} else {
|
||||
setItem(DATE_RANGE_CONFIG, value);
|
||||
setDateRange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return [dateRange, saveDateRange];
|
||||
}
|
||||
|
||||
export default useDateRange;
|
||||
15
src/components/hooks/useDocumentClick.js
Normal file
15
src/components/hooks/useDocumentClick.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export function useDocumentClick(handler) {
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handler);
|
||||
};
|
||||
}, [handler]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default useDocumentClick;
|
||||
21
src/components/hooks/useEscapeKey.js
Normal file
21
src/components/hooks/useEscapeKey.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export default useEscapeKey;
|
||||
47
src/components/hooks/useFilters.js
Normal file
47
src/components/hooks/useFilters.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useMessages } from './useMessages';
|
||||
import { OPERATORS } from 'lib/constants';
|
||||
|
||||
export function useFilters() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const filterLabels = {
|
||||
[OPERATORS.equals]: formatMessage(labels.is),
|
||||
[OPERATORS.notEquals]: formatMessage(labels.isNot),
|
||||
[OPERATORS.set]: formatMessage(labels.isSet),
|
||||
[OPERATORS.notSet]: formatMessage(labels.isNotSet),
|
||||
[OPERATORS.contains]: formatMessage(labels.contains),
|
||||
[OPERATORS.doesNotContain]: formatMessage(labels.doesNotContain),
|
||||
[OPERATORS.true]: formatMessage(labels.true),
|
||||
[OPERATORS.false]: formatMessage(labels.false),
|
||||
[OPERATORS.greaterThan]: formatMessage(labels.greaterThan),
|
||||
[OPERATORS.lessThan]: formatMessage(labels.lessThan),
|
||||
[OPERATORS.greaterThanEquals]: formatMessage(labels.greaterThanEquals),
|
||||
[OPERATORS.lessThanEquals]: formatMessage(labels.lessThanEquals),
|
||||
[OPERATORS.before]: formatMessage(labels.before),
|
||||
[OPERATORS.after]: formatMessage(labels.after),
|
||||
};
|
||||
|
||||
const typeFilters = {
|
||||
string: [OPERATORS.equals, OPERATORS.notEquals],
|
||||
array: [OPERATORS.contains, OPERATORS.doesNotContain],
|
||||
boolean: [OPERATORS.true, OPERATORS.false],
|
||||
number: [
|
||||
OPERATORS.equals,
|
||||
OPERATORS.notEquals,
|
||||
OPERATORS.greaterThan,
|
||||
OPERATORS.lessThan,
|
||||
OPERATORS.greaterThanEquals,
|
||||
OPERATORS.lessThanEquals,
|
||||
],
|
||||
date: [OPERATORS.before, OPERATORS.after],
|
||||
uuid: [OPERATORS.equals],
|
||||
};
|
||||
|
||||
const getFilters = type => {
|
||||
return typeFilters[type]?.map(key => ({ type, value: key, label: filterLabels[key] })) ?? [];
|
||||
};
|
||||
|
||||
return { getFilters, filterLabels, typeFilters };
|
||||
}
|
||||
|
||||
export default useFilters;
|
||||
11
src/components/hooks/useForceUpdate.js
Normal file
11
src/components/hooks/useForceUpdate.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
export function useForceUpdate() {
|
||||
const [, update] = useState(Object.create(null));
|
||||
|
||||
return useCallback(() => {
|
||||
update(Object.create(null));
|
||||
}, [update]);
|
||||
}
|
||||
|
||||
export default useForceUpdate;
|
||||
39
src/components/hooks/useFormat.js
Normal file
39
src/components/hooks/useFormat.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import useMessages from './useMessages';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import useLocale from './useLocale';
|
||||
import useCountryNames from './useCountryNames';
|
||||
|
||||
export function useFormat() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
|
||||
const formatBrowser = value => {
|
||||
return BROWSERS[value] || value;
|
||||
};
|
||||
|
||||
const formatCountry = value => {
|
||||
return countryNames[value] || value;
|
||||
};
|
||||
|
||||
const formatDevice = value => {
|
||||
return formatMessage(labels[value] || labels.unknown);
|
||||
};
|
||||
|
||||
const formatValue = (value, type) => {
|
||||
switch (type) {
|
||||
case 'browser':
|
||||
return formatBrowser(value);
|
||||
case 'country':
|
||||
return formatCountry(value);
|
||||
case 'device':
|
||||
return formatDevice(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
return { formatBrowser, formatCountry, formatDevice, formatValue };
|
||||
}
|
||||
|
||||
export default useFormat;
|
||||
36
src/components/hooks/useLanguageNames.js
Normal file
36
src/components/hooks/useLanguageNames.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { httpGet } from 'next-basics';
|
||||
import enUS from 'public/intl/language/en-US.json';
|
||||
|
||||
const languageNames = {
|
||||
'en-US': enUS,
|
||||
};
|
||||
|
||||
export function useLanguageNames(locale) {
|
||||
const [list, setList] = useState(languageNames[locale] || enUS);
|
||||
const { basePath } = useRouter();
|
||||
|
||||
async function loadData(locale) {
|
||||
const { data } = await httpGet(`${basePath}/intl/language/${locale}.json`);
|
||||
|
||||
if (data) {
|
||||
languageNames[locale] = data;
|
||||
setList(languageNames[locale]);
|
||||
} else {
|
||||
setList(enUS);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!languageNames[locale]) {
|
||||
loadData(locale);
|
||||
} else {
|
||||
setList(languageNames[locale]);
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
export default useLanguageNames;
|
||||
65
src/components/hooks/useLocale.js
Normal file
65
src/components/hooks/useLocale.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { httpGet, setItem } from 'next-basics';
|
||||
import { LOCALE_CONFIG } from 'lib/constants';
|
||||
import { getDateLocale, getTextDirection } from 'lib/lang';
|
||||
import useStore, { setLocale } from 'store/app';
|
||||
import useForceUpdate from 'components/hooks/useForceUpdate';
|
||||
import enUS from 'public/intl/messages/en-US.json';
|
||||
|
||||
const messages = {
|
||||
'en-US': enUS,
|
||||
};
|
||||
|
||||
const selector = state => state.locale;
|
||||
|
||||
export function useLocale() {
|
||||
const locale = useStore(selector);
|
||||
const { basePath } = useRouter();
|
||||
const forceUpdate = useForceUpdate();
|
||||
const dir = getTextDirection(locale);
|
||||
const dateLocale = getDateLocale(locale);
|
||||
|
||||
async function loadMessages(locale) {
|
||||
const { ok, data } = await httpGet(`${basePath}/intl/messages/${locale}.json`);
|
||||
|
||||
if (ok) {
|
||||
messages[locale] = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLocale(value) {
|
||||
if (!messages[value]) {
|
||||
await loadMessages(value);
|
||||
}
|
||||
|
||||
setItem(LOCALE_CONFIG, value);
|
||||
|
||||
document.getElementById('__next')?.setAttribute('dir', getTextDirection(value));
|
||||
|
||||
if (locale !== value) {
|
||||
setLocale(value);
|
||||
} else {
|
||||
forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!messages[locale]) {
|
||||
saveLocale(locale);
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window?.location?.href);
|
||||
const locale = url.searchParams.get('locale');
|
||||
|
||||
if (locale) {
|
||||
saveLocale(locale);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { locale, saveLocale, messages, dir, dateLocale };
|
||||
}
|
||||
|
||||
export default useLocale;
|
||||
20
src/components/hooks/useMessages.js
Normal file
20
src/components/hooks/useMessages.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { messages, labels } from 'components/messages';
|
||||
|
||||
export function useMessages() {
|
||||
const intl = useIntl();
|
||||
|
||||
const getMessage = id => {
|
||||
const message = Object.values(messages).find(value => value.id === id);
|
||||
|
||||
return message ? formatMessage(message) : id;
|
||||
};
|
||||
|
||||
const formatMessage = (descriptor, values, opts) => {
|
||||
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
|
||||
};
|
||||
|
||||
return { formatMessage, FormattedMessage, messages, labels, getMessage };
|
||||
}
|
||||
|
||||
export default useMessages;
|
||||
33
src/components/hooks/usePageQuery.js
Normal file
33
src/components/hooks/usePageQuery.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { buildUrl } from 'next-basics';
|
||||
|
||||
export function usePageQuery() {
|
||||
const router = useRouter();
|
||||
const { pathname, search } = location;
|
||||
const { asPath } = router;
|
||||
|
||||
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 resolveUrl(params, reset) {
|
||||
return buildUrl(asPath.split('?')[0], { ...(reset ? {} : query), ...params });
|
||||
}
|
||||
|
||||
return { pathname, query, resolveUrl, router };
|
||||
}
|
||||
|
||||
export default usePageQuery;
|
||||
86
src/components/hooks/useReport.js
Normal file
86
src/components/hooks/useReport.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { produce } from 'immer';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTimezone } from './useTimezone';
|
||||
import useApi from './useApi';
|
||||
|
||||
const baseParameters = {
|
||||
name: 'Untitled',
|
||||
description: '',
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export function useReport(reportId, defaultParameters) {
|
||||
const [report, setReport] = useState(null);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const { get, post } = useApi();
|
||||
const [timezone] = useTimezone();
|
||||
|
||||
const loadReport = async id => {
|
||||
const data = await get(`/reports/${id}`);
|
||||
|
||||
const { dateRange } = data?.parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
|
||||
if (startDate && endDate) {
|
||||
dateRange.startDate = new Date(startDate);
|
||||
dateRange.endDate = new Date(endDate);
|
||||
}
|
||||
|
||||
setReport(data);
|
||||
};
|
||||
|
||||
const runReport = useCallback(
|
||||
async parameters => {
|
||||
setIsRunning(true);
|
||||
|
||||
const { type } = report;
|
||||
|
||||
const data = await post(`/reports/${type}`, { ...parameters, timezone });
|
||||
|
||||
setReport(
|
||||
produce(state => {
|
||||
state.parameters = parameters;
|
||||
state.data = data;
|
||||
|
||||
return state;
|
||||
}),
|
||||
);
|
||||
|
||||
setIsRunning(false);
|
||||
},
|
||||
[report],
|
||||
);
|
||||
|
||||
const updateReport = useCallback(
|
||||
async data => {
|
||||
setReport(
|
||||
produce(state => {
|
||||
const { parameters, ...rest } = data;
|
||||
|
||||
if (parameters) {
|
||||
state.parameters = { ...state.parameters, ...parameters };
|
||||
}
|
||||
|
||||
for (const key in rest) {
|
||||
state[key] = rest[key];
|
||||
}
|
||||
|
||||
return state;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[report],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reportId) {
|
||||
setReport({ ...baseParameters, ...defaultParameters });
|
||||
} else {
|
||||
loadReport(reportId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { report, runReport, updateReport, isRunning };
|
||||
}
|
||||
|
||||
export default useReport;
|
||||
38
src/components/hooks/useReports.js
Normal file
38
src/components/hooks/useReports.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useState } from 'react';
|
||||
import useApi from './useApi';
|
||||
import useApiFilter from 'components/hooks/useApiFilter';
|
||||
|
||||
export function useReports() {
|
||||
const [modified, setModified] = useState(Date.now());
|
||||
const { get, useQuery, del, useMutation } = useApi();
|
||||
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['reports', { modified, filter, page, pageSize }],
|
||||
() => get(`/reports`, { filter, page, pageSize }),
|
||||
);
|
||||
|
||||
const deleteReport = id => {
|
||||
mutate(id, {
|
||||
onSuccess: () => {
|
||||
setModified(Date.now());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
reports: data,
|
||||
error,
|
||||
isLoading,
|
||||
deleteReport,
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default useReports;
|
||||
30
src/components/hooks/useRequireLogin.js
Normal file
30
src/components/hooks/useRequireLogin.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
|
||||
export function useRequireLogin() {
|
||||
const router = useRouter();
|
||||
const { get } = useApi();
|
||||
const { user, setUser } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
async function loadUser() {
|
||||
try {
|
||||
const { user } = await get('/auth/verify');
|
||||
|
||||
setUser(user);
|
||||
} catch {
|
||||
await router.push('/login');
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
loadUser();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return { user };
|
||||
}
|
||||
|
||||
export default useRequireLogin;
|
||||
28
src/components/hooks/useShareToken.js
Normal file
28
src/components/hooks/useShareToken.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useEffect } from 'react';
|
||||
import useStore, { setShareToken } from 'store/app';
|
||||
import useApi from './useApi';
|
||||
|
||||
const selector = state => state.shareToken;
|
||||
|
||||
export function useShareToken(shareId) {
|
||||
const shareToken = useStore(selector);
|
||||
const { get } = useApi();
|
||||
|
||||
async function loadToken(id) {
|
||||
const data = await get(`/share/${id}`);
|
||||
|
||||
if (data) {
|
||||
setShareToken(data);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (shareId) {
|
||||
loadToken(shareId);
|
||||
}
|
||||
}, [shareId]);
|
||||
|
||||
return shareToken;
|
||||
}
|
||||
|
||||
export default useShareToken;
|
||||
25
src/components/hooks/useSticky.js
Normal file
25
src/components/hooks/useSticky.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export function useSticky({ enabled = true, threshold = 1 }) {
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
let observer;
|
||||
const handler = ([entry]) => setIsSticky(entry.intersectionRatio < threshold);
|
||||
|
||||
if (enabled && ref.current) {
|
||||
observer = new IntersectionObserver(handler, { threshold: [threshold] });
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
return () => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}, [ref, enabled, threshold]);
|
||||
|
||||
return { ref, isSticky };
|
||||
}
|
||||
|
||||
export default useSticky;
|
||||
68
src/components/hooks/useTheme.js
Normal file
68
src/components/hooks/useTheme.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { useEffect } from 'react';
|
||||
import useStore, { setTheme } from 'store/app';
|
||||
import { getItem, setItem } from 'next-basics';
|
||||
import { THEME_COLORS, THEME_CONFIG } from 'lib/constants';
|
||||
import { colord } from 'colord';
|
||||
|
||||
const selector = state => state.theme;
|
||||
|
||||
export function useTheme() {
|
||||
const defaultTheme =
|
||||
typeof window !== 'undefined'
|
||||
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
: 'light';
|
||||
const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
|
||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||
|
||||
const colors = {
|
||||
theme: {
|
||||
...THEME_COLORS[theme],
|
||||
},
|
||||
chart: {
|
||||
text: THEME_COLORS[theme].gray700,
|
||||
line: THEME_COLORS[theme].gray200,
|
||||
views: {
|
||||
hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
|
||||
backgroundColor: primaryColor.alpha(0.4).toRgbString(),
|
||||
borderColor: primaryColor.alpha(0.7).toRgbString(),
|
||||
hoverBorderColor: primaryColor.toRgbString(),
|
||||
},
|
||||
visitors: {
|
||||
hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
|
||||
backgroundColor: primaryColor.alpha(0.6).toRgbString(),
|
||||
borderColor: primaryColor.alpha(0.9).toRgbString(),
|
||||
hoverBorderColor: primaryColor.toRgbString(),
|
||||
},
|
||||
},
|
||||
map: {
|
||||
baseColor: THEME_COLORS[theme].primary,
|
||||
fillColor: THEME_COLORS[theme].gray100,
|
||||
strokeColor: THEME_COLORS[theme].primary,
|
||||
hoverColor: THEME_COLORS[theme].primary,
|
||||
},
|
||||
};
|
||||
|
||||
function saveTheme(value) {
|
||||
setItem(THEME_CONFIG, value);
|
||||
setTheme(value);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('data-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window?.location?.href);
|
||||
const theme = url.searchParams.get('theme');
|
||||
|
||||
if (['light', 'dark'].includes(theme)) {
|
||||
saveTheme(theme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { theme, saveTheme, colors };
|
||||
}
|
||||
|
||||
export default useTheme;
|
||||
20
src/components/hooks/useTimezone.js
Normal file
20
src/components/hooks/useTimezone.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { getTimezone } from 'lib/date';
|
||||
import { getItem, setItem } from 'next-basics';
|
||||
import { TIMEZONE_CONFIG } from 'lib/constants';
|
||||
|
||||
export function useTimezone() {
|
||||
const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone());
|
||||
|
||||
const saveTimezone = useCallback(
|
||||
value => {
|
||||
setItem(TIMEZONE_CONFIG, value);
|
||||
setTimezone(value);
|
||||
},
|
||||
[setTimezone],
|
||||
);
|
||||
|
||||
return [timezone, saveTimezone];
|
||||
}
|
||||
|
||||
export default useTimezone;
|
||||
11
src/components/hooks/useUser.js
Normal file
11
src/components/hooks/useUser.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import useStore, { setUser } from 'store/app';
|
||||
|
||||
const selector = state => state.user;
|
||||
|
||||
export function useUser() {
|
||||
const user = useStore(selector);
|
||||
|
||||
return { user, setUser };
|
||||
}
|
||||
|
||||
export default useUser;
|
||||
10
src/components/hooks/useWebsite.js
Normal file
10
src/components/hooks/useWebsite.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import useApi from './useApi';
|
||||
|
||||
export function useWebsite(websiteId) {
|
||||
const { get, useQuery } = useApi();
|
||||
return useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), {
|
||||
enabled: !!websiteId,
|
||||
});
|
||||
}
|
||||
|
||||
export default useWebsite;
|
||||
38
src/components/hooks/useWebsiteReports.js
Normal file
38
src/components/hooks/useWebsiteReports.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useState } from 'react';
|
||||
import useApi from './useApi';
|
||||
import useApiFilter from 'components/hooks/useApiFilter';
|
||||
|
||||
export function useWebsiteReports(websiteId) {
|
||||
const [modified, setModified] = useState(Date.now());
|
||||
const { get, useQuery, del, useMutation } = useApi();
|
||||
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['reports:website', { websiteId, modified, filter, page, pageSize }],
|
||||
() => get(`/websites/${websiteId}/reports`, { websiteId, filter, page, pageSize }),
|
||||
);
|
||||
|
||||
const deleteReport = id => {
|
||||
mutate(id, {
|
||||
onSuccess: () => {
|
||||
setModified(Date.now());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
reports: data,
|
||||
error,
|
||||
isLoading,
|
||||
deleteReport,
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default useWebsiteReports;
|
||||
Loading…
Add table
Add a link
Reference in a new issue