mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 05:07:15 +01:00
Merge branch 'dev' into os-names
This commit is contained in:
commit
37e28bbf74
472 changed files with 5460 additions and 5026 deletions
71
src/components/common/Avatar.tsx
Normal file
71
src/components/common/Avatar.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import md5 from 'md5';
|
||||
import { colord, extend } from 'colord';
|
||||
import harmoniesPlugin from 'colord/plugins/harmonies';
|
||||
import mixPlugin from 'colord/plugins/mix';
|
||||
|
||||
extend([harmoniesPlugin, mixPlugin]);
|
||||
|
||||
const harmonies = [
|
||||
//'analogous',
|
||||
//'complementary',
|
||||
'double-split-complementary',
|
||||
//'rectangle',
|
||||
'split-complementary',
|
||||
'tetradic',
|
||||
//'triadic',
|
||||
];
|
||||
|
||||
const color = (value: string, invert: boolean = false) => {
|
||||
const c = colord(value.startsWith('#') ? value : `#${value}`);
|
||||
|
||||
if (invert && c.isDark()) {
|
||||
return c.invert();
|
||||
}
|
||||
|
||||
return c;
|
||||
};
|
||||
|
||||
const remix = (hash: string) => {
|
||||
const a = hash.substring(0, 6);
|
||||
const b = hash.substring(6, 12);
|
||||
const c = hash.substring(12, 18);
|
||||
const d = hash.substring(18, 24);
|
||||
const e = hash.substring(24, 30);
|
||||
const f = hash.substring(30, 32);
|
||||
|
||||
const base = [b, c, d, e]
|
||||
.reduce((acc, val) => {
|
||||
return acc.mix(color(val), 0.05);
|
||||
}, color(a))
|
||||
.saturate(0.1)
|
||||
.toHex();
|
||||
|
||||
const harmony = pick(parseInt(f, 16), harmonies);
|
||||
|
||||
return color(base, true)
|
||||
.harmonies(harmony)
|
||||
.map(c => c.toHex());
|
||||
};
|
||||
|
||||
const pick = (num: number, arr: any[]) => {
|
||||
return arr[num % arr.length];
|
||||
};
|
||||
|
||||
export function Avatar({ value }: { value: string }) {
|
||||
const hash = md5(value);
|
||||
const colors = remix(hash);
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id={`color-${hash}`} gradientTransform="rotate(90)">
|
||||
<stop offset="0%" stopColor={colors[1]} />
|
||||
<stop offset="100%" stopColor={colors[2]} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill={`url(#color-${hash})`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Avatar;
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, LoadingButton, Form, FormButtons } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export interface ConfirmDeleteFormProps {
|
||||
name: string;
|
||||
onConfirm?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmDeleteForm({ name, onConfirm, onClose }: ConfirmDeleteFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
|
||||
const handleConfirm = () => {
|
||||
setLoading(true);
|
||||
onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<p>
|
||||
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{name}</b> }} />
|
||||
</p>
|
||||
<FormButtons flex>
|
||||
<LoadingButton isLoading={loading} onClick={handleConfirm} variant="danger">
|
||||
{formatMessage(labels.delete)}
|
||||
</LoadingButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmDeleteForm;
|
||||
39
src/components/common/ConfirmationForm.tsx
Normal file
39
src/components/common/ConfirmationForm.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Button, LoadingButton, Form, FormButtons } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export interface ConfirmationFormProps {
|
||||
message: ReactNode;
|
||||
buttonLabel?: ReactNode;
|
||||
buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger';
|
||||
isLoading?: boolean;
|
||||
error?: string | Error;
|
||||
onConfirm?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmationForm({
|
||||
message,
|
||||
buttonLabel,
|
||||
buttonVariant,
|
||||
isLoading,
|
||||
error,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: ConfirmationFormProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<Form error={error}>
|
||||
<p>{message}</p>
|
||||
<FormButtons flex>
|
||||
<LoadingButton isLoading={isLoading} onClick={onConfirm} variant={buttonVariant}>
|
||||
{buttonLabel || formatMessage(labels.ok)}
|
||||
</LoadingButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmationForm;
|
||||
|
|
@ -29,10 +29,6 @@
|
|||
gap: 10px;
|
||||
min-height: 70px;
|
||||
align-items: center;
|
||||
min-width: min-content;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.body > div > div > div {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useMessages } from 'components/hooks';
|
|||
import Empty from 'components/common/Empty';
|
||||
import Pager from 'components/common/Pager';
|
||||
import styles from './DataTable.module.css';
|
||||
import { FilterQueryResult } from 'components/hooks/useFilterQuery';
|
||||
import { FilterQueryResult } from 'lib/types';
|
||||
|
||||
const DEFAULT_SEARCH_DELAY = 600;
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ export function DataTable({
|
|||
className={classNames(styles.body, { [styles.status]: isLoading || noResults || !hasData })}
|
||||
>
|
||||
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
|
||||
{isLoading && <Loading icon="dots" />}
|
||||
{isLoading && <Loading position="page" />}
|
||||
{!isLoading && !hasData && !query && <Empty />}
|
||||
{noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@
|
|||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'classnames';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './Empty.module.css';
|
||||
|
||||
export interface EmptyProps {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ErrorInfo, ReactNode } from 'react';
|
||||
import { ErrorBoundary as Boundary } from 'react-error-boundary';
|
||||
import { Button } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './ErrorBoundary.module.css';
|
||||
|
||||
const logError = (error: Error, info: ErrorInfo) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Icon, Icons, Text } from 'react-basics';
|
||||
import styles from './ErrorMessage.module.css';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function ErrorMessage() {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { Icon, Icons } from 'react-basics';
|
|||
import classNames from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import { safeDecodeURI } from 'next-basics';
|
||||
import useNavigation from 'components/hooks/useNavigation';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useNavigation } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './FilterLink.module.css';
|
||||
|
||||
export interface FilterLinkProps {
|
||||
|
|
@ -25,7 +25,7 @@ export function FilterLink({
|
|||
className,
|
||||
}: FilterLinkProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { makeUrl, query } = useNavigation();
|
||||
const { renderUrl, query } = useNavigation();
|
||||
const active = query[id] !== undefined;
|
||||
const selected = query[id] === value;
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ export function FilterLink({
|
|||
{children}
|
||||
{!value && `(${label || formatMessage(labels.unknown)})`}
|
||||
{value && (
|
||||
<Link href={makeUrl({ [id]: value })} className={styles.label} replace>
|
||||
<Link href={renderUrl({ [id]: value })} className={styles.label} replace>
|
||||
{safeDecodeURI(label || value)}
|
||||
</Link>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
import { Button, Icon, Icons } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './Pager.module.css';
|
||||
|
||||
export interface PagerProps {
|
||||
|
|
|
|||
58
src/components/common/TypeConfirmationForm.tsx
Normal file
58
src/components/common/TypeConfirmationForm.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormRow,
|
||||
FormInput,
|
||||
TextField,
|
||||
SubmitButton,
|
||||
} from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function TypeConfirmationForm({
|
||||
confirmationValue,
|
||||
buttonLabel,
|
||||
buttonVariant,
|
||||
isLoading,
|
||||
error,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: {
|
||||
confirmationValue: string;
|
||||
buttonLabel?: string;
|
||||
buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger';
|
||||
isLoading?: boolean;
|
||||
error?: string | Error;
|
||||
onConfirm?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
|
||||
if (!confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={onConfirm} error={error}>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
{...messages.actionConfirmation}
|
||||
values={{ confirmation: <b>{confirmationValue}</b> }}
|
||||
/>
|
||||
</p>
|
||||
<FormRow label={formatMessage(labels.confirm)}>
|
||||
<FormInput name="confirm" rules={{ validate: value => value === confirmationValue }}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormButtons flex>
|
||||
<SubmitButton isLoading={isLoading} variant={buttonVariant}>
|
||||
{buttonLabel || formatMessage(labels.ok)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default TypeConfirmationForm;
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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 './useNavigation';
|
||||
export * from './useReport';
|
||||
export * from './useReports';
|
||||
export * from './useLogin';
|
||||
export * from './useShareToken';
|
||||
export * from './useSticky';
|
||||
export * from './useTheme';
|
||||
export * from './useTimezone';
|
||||
export * from './useUser';
|
||||
export * from './useWebsite';
|
||||
33
src/components/hooks/index.ts
Normal file
33
src/components/hooks/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export * from './queries/useApi';
|
||||
export * from './queries/useConfig';
|
||||
export * from './queries/useFilterQuery';
|
||||
export * from './queries/useLogin';
|
||||
export * from './queries/useReport';
|
||||
export * from './queries/useReports';
|
||||
export * from './queries/useShareToken';
|
||||
export * from './queries/useTeam';
|
||||
export * from './queries/useTeams';
|
||||
export * from './queries/useTeamWebsites';
|
||||
export * from './queries/useTeamMembers';
|
||||
export * from './queries/useUser';
|
||||
export * from './queries/useUsers';
|
||||
export * from './queries/useWebsite';
|
||||
export * from './queries/useWebsites';
|
||||
export * from './queries/useWebsiteEvents';
|
||||
export * from './queries/useWebsiteMetrics';
|
||||
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 './useModified';
|
||||
export * from './useNavigation';
|
||||
export * from './useSticky';
|
||||
export * from './useTeamUrl';
|
||||
export * from './useTheme';
|
||||
export * from './useTimezone';
|
||||
|
|
@ -4,7 +4,7 @@ import { getClientAuthToken } from 'lib/client';
|
|||
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||
import useStore from 'store/app';
|
||||
|
||||
const selector = state => state.shareToken;
|
||||
const selector = (state: { shareToken: { token?: string } }) => state.shareToken;
|
||||
|
||||
export function useApi() {
|
||||
const shareToken = useStore(selector);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import useStore, { setConfig } from 'store/app';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { useApi } from './useApi';
|
||||
|
||||
let loading = false;
|
||||
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useState, Dispatch, SetStateAction } from 'react';
|
||||
import { useApi } from 'components/hooks/useApi';
|
||||
import { FilterResult, SearchFilter } from 'lib/types';
|
||||
import { useState } from 'react';
|
||||
import { useApi } from './useApi';
|
||||
import { FilterResult, SearchFilter, FilterQueryResult } from 'lib/types';
|
||||
|
||||
export interface FilterQueryResult<T> {
|
||||
result: FilterResult<T>;
|
||||
query: any;
|
||||
params: SearchFilter;
|
||||
setParams: Dispatch<SetStateAction<T | SearchFilter>>;
|
||||
}
|
||||
|
||||
export function useFilterQuery<T>({
|
||||
export function useFilterQuery<T = any>({
|
||||
queryKey,
|
||||
queryFn,
|
||||
...options
|
||||
29
src/components/hooks/queries/useLogin.ts
Normal file
29
src/components/hooks/queries/useLogin.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import useStore, { setUser } from 'store/app';
|
||||
import useApi from './useApi';
|
||||
import { UseQueryResult } from '@tanstack/react-query';
|
||||
|
||||
const selector = (state: { user: any }) => state.user;
|
||||
|
||||
export function useLogin(): {
|
||||
user: any;
|
||||
setUser: (data: any) => void;
|
||||
} & UseQueryResult {
|
||||
const { get, useQuery } = useApi();
|
||||
const user = useStore(selector);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['login'],
|
||||
queryFn: async () => {
|
||||
const data = await get('/auth/verify');
|
||||
|
||||
setUser(data);
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: !user,
|
||||
});
|
||||
|
||||
return { user, setUser, ...query };
|
||||
}
|
||||
|
||||
export default useLogin;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { produce } from 'immer';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTimezone } from './useTimezone';
|
||||
import useApi from './useApi';
|
||||
import useMessages from './useMessages';
|
||||
import { useApi } from './useApi';
|
||||
import { useTimezone } from '../useTimezone';
|
||||
import { useMessages } from '../useMessages';
|
||||
|
||||
export function useReport(reportId: string, defaultParameters: { [key: string]: any }) {
|
||||
const [report, setReport] = useState(null);
|
||||
28
src/components/hooks/queries/useReports.ts
Normal file
28
src/components/hooks/queries/useReports.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useReports({ websiteId, teamId }: { websiteId?: string; teamId?: string }) {
|
||||
const { modified } = useModified(`reports`);
|
||||
const { get, del, useMutation } = useApi();
|
||||
const queryResult = useFilterQuery({
|
||||
queryKey: ['reports', { websiteId, teamId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get('/reports', { websiteId, teamId, ...params });
|
||||
},
|
||||
});
|
||||
const { mutate } = useMutation({ mutationFn: (reportId: string) => del(`/reports/${reportId}`) });
|
||||
|
||||
const deleteReport = (reportId: any) => {
|
||||
mutate(reportId, {
|
||||
onSuccess: () => {},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...queryResult,
|
||||
deleteReport,
|
||||
};
|
||||
}
|
||||
|
||||
export default useReports;
|
||||
12
src/components/hooks/queries/useTeam.ts
Normal file
12
src/components/hooks/queries/useTeam.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import useApi from './useApi';
|
||||
|
||||
export function useTeam(teamId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['teams', teamId],
|
||||
queryFn: () => get(`/teams/${teamId}`),
|
||||
enabled: !!teamId,
|
||||
});
|
||||
}
|
||||
|
||||
export default useTeam;
|
||||
18
src/components/hooks/queries/useTeamMembers.ts
Normal file
18
src/components/hooks/queries/useTeamMembers.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeamMembers(teamId: string) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`teams:members`);
|
||||
|
||||
return useFilterQuery({
|
||||
queryKey: ['teams:members', { teamId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/teams/${teamId}/users`, params);
|
||||
},
|
||||
enabled: !!teamId,
|
||||
});
|
||||
}
|
||||
|
||||
export default useTeamMembers;
|
||||
17
src/components/hooks/queries/useTeamWebsites.ts
Normal file
17
src/components/hooks/queries/useTeamWebsites.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeamWebsites(teamId: string) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`teams:websites`);
|
||||
|
||||
return useFilterQuery({
|
||||
queryKey: ['teams:websites', { teamId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/teams/${teamId}/websites`, params);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default useTeamWebsites;
|
||||
20
src/components/hooks/queries/useTeams.ts
Normal file
20
src/components/hooks/queries/useTeams.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import useLogin from './useLogin';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeams(userId?: string) {
|
||||
const { get } = useApi();
|
||||
const { user } = useLogin();
|
||||
const id = userId || user?.id;
|
||||
const { modified } = useModified(`teams`);
|
||||
|
||||
return useFilterQuery({
|
||||
queryKey: ['teams', { userId: id, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/teams`, params);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default useTeams;
|
||||
13
src/components/hooks/queries/useUser.ts
Normal file
13
src/components/hooks/queries/useUser.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import useApi from './useApi';
|
||||
|
||||
export function useUser(userId: string, options?: { [key: string]: any }) {
|
||||
const { get, useQuery } = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: () => get(`/users/${userId}`),
|
||||
enabled: !!userId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useUser;
|
||||
19
src/components/hooks/queries/useUsers.ts
Normal file
19
src/components/hooks/queries/useUsers.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useUsers() {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`users`);
|
||||
|
||||
return useFilterQuery({
|
||||
queryKey: ['users', { modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get('/admin/users', {
|
||||
...params,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default useUsers;
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import useApi from './useApi';
|
||||
|
||||
export function useWebsite(websiteId: string) {
|
||||
export function useWebsite(websiteId: string, options?: { [key: string]: any }) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites', websiteId],
|
||||
queryKey: ['website', { websiteId }],
|
||||
queryFn: () => get(`/websites/${websiteId}`),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
19
src/components/hooks/queries/useWebsiteEvents.ts
Normal file
19
src/components/hooks/queries/useWebsiteEvents.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
|
||||
export function useWebsiteEvents(
|
||||
websiteId: string,
|
||||
params?: { [key: string]: any },
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['events', { ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/events`, { ...params }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useWebsiteEvents;
|
||||
36
src/components/hooks/queries/useWebsiteMetrics.ts
Normal file
36
src/components/hooks/queries/useWebsiteMetrics.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
|
||||
export function useWebsiteMetrics(
|
||||
websiteId: string,
|
||||
params?: { [key: string]: any },
|
||||
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
'websites:metrics',
|
||||
{
|
||||
websiteId,
|
||||
...params,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
const filters = { ...params };
|
||||
|
||||
filters[params.type] = undefined;
|
||||
|
||||
const data = await get(`/websites/${websiteId}/metrics`, {
|
||||
...filters,
|
||||
});
|
||||
|
||||
options?.onDataLoad?.(data);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useWebsiteMetrics;
|
||||
25
src/components/hooks/queries/useWebsites.ts
Normal file
25
src/components/hooks/queries/useWebsites.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useFilterQuery } from './useFilterQuery';
|
||||
import { useLogin } from './useLogin';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useWebsites(
|
||||
{ userId, teamId }: { userId?: string; teamId?: string },
|
||||
params?: { [key: string]: string | number },
|
||||
) {
|
||||
const { get } = useApi();
|
||||
const { user } = useLogin();
|
||||
const { modified } = useModified(`websites`);
|
||||
|
||||
return useFilterQuery({
|
||||
queryKey: ['websites', { userId, teamId, modified, ...params }],
|
||||
queryFn: (data: any) => {
|
||||
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
|
||||
...data,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default useWebsites;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { httpGet } from 'next-basics';
|
||||
import enUS from 'public/intl/country/en-US.json';
|
||||
import enUS from '../../../public/intl/country/en-US.json';
|
||||
|
||||
const countryNames = {
|
||||
'en-US': enUS,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
|||
import websiteStore, { setWebsiteDateRange } from 'store/websites';
|
||||
import appStore, { setDateRange } from 'store/app';
|
||||
import { DateRange } from 'lib/types';
|
||||
import useLocale from './useLocale';
|
||||
import useApi from './useApi';
|
||||
import { useLocale } from './useLocale';
|
||||
import { useApi } from './queries/useApi';
|
||||
|
||||
export function useDateRange(websiteId?: string) {
|
||||
export function useDateRange(websiteId?: string): [DateRange, (value: string | DateRange) => void] {
|
||||
const { get } = useApi();
|
||||
const { locale } = useLocale();
|
||||
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
|
||||
|
|
@ -45,10 +45,7 @@ export function useDateRange(websiteId?: string) {
|
|||
}
|
||||
};
|
||||
|
||||
return [dateRange, saveDateRange] as [
|
||||
{ startDate: Date; endDate: Date; modified?: number },
|
||||
(value: string | DateRange) => void,
|
||||
];
|
||||
return [dateRange, saveDateRange];
|
||||
}
|
||||
|
||||
export default useDateRange;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import useMessages from './useMessages';
|
|||
import { BROWSERS, OS_NAMES } from 'lib/constants';
|
||||
import useLocale from './useLocale';
|
||||
import useCountryNames from './useCountryNames';
|
||||
import regions from 'public/iso-3166-2.json';
|
||||
import regions from '../../../public/iso-3166-2.json';
|
||||
|
||||
export function useFormat() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -31,7 +31,7 @@ export function useFormat() {
|
|||
};
|
||||
|
||||
const formatCity = (value: string, country?: string): string => {
|
||||
return `${value}, ${countryNames[country]}`;
|
||||
return countryNames[country] ? `${value}, ${countryNames[country]}` : value;
|
||||
};
|
||||
|
||||
const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { httpGet } from 'next-basics';
|
||||
import enUS from 'public/intl/language/en-US.json';
|
||||
import enUS from '../../../public/intl/language/en-US.json';
|
||||
|
||||
const languageNames = {
|
||||
'en-US': enUS,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ 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/country/en-US.json';
|
||||
import { useForceUpdate } from './useForceUpdate';
|
||||
import enUS from '../../../public/intl/country/en-US.json';
|
||||
|
||||
const messages = {
|
||||
'en-US': enUS,
|
||||
};
|
||||
|
||||
const selector = state => state.locale;
|
||||
const selector = (state: { locale: any }) => state.locale;
|
||||
|
||||
export function useLocale() {
|
||||
const locale = useStore(selector);
|
||||
|
|
@ -18,7 +18,7 @@ export function useLocale() {
|
|||
const dir = getTextDirection(locale);
|
||||
const dateLocale = getDateLocale(locale);
|
||||
|
||||
async function loadMessages(locale) {
|
||||
async function loadMessages(locale: string) {
|
||||
const { ok, data } = await httpGet(`${process.env.basePath}/intl/messages/${locale}.json`);
|
||||
|
||||
if (ok) {
|
||||
|
|
@ -26,7 +26,7 @@ export function useLocale() {
|
|||
}
|
||||
}
|
||||
|
||||
async function saveLocale(value) {
|
||||
async function saveLocale(value: string) {
|
||||
if (!messages[value]) {
|
||||
await loadMessages(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import useApi from 'components/hooks/useApi';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
|
||||
export function useLogin() {
|
||||
const { get, useQuery } = useApi();
|
||||
const { user, setUser } = useUser();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['login'],
|
||||
queryFn: async () => {
|
||||
const data = await get('/auth/verify');
|
||||
|
||||
setUser(data);
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
return { user, ...query };
|
||||
}
|
||||
|
||||
export default useLogin;
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { useIntl, FormattedMessage, MessageDescriptor, PrimitiveType } from 'react-intl';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { messages, labels } from 'components/messages';
|
||||
import { FormatXMLElementFn, Options } from 'intl-messageformat';
|
||||
|
||||
export function useMessages(): any {
|
||||
const intl = useIntl();
|
||||
|
|
@ -12,14 +11,12 @@ export function useMessages(): any {
|
|||
};
|
||||
|
||||
const formatMessage = (
|
||||
descriptor:
|
||||
| MessageDescriptor
|
||||
| {
|
||||
id: string;
|
||||
defaultMessage: string;
|
||||
},
|
||||
values?: Record<string, PrimitiveType | FormatXMLElementFn<string, string>>,
|
||||
opts?: Options,
|
||||
descriptor: {
|
||||
id: string;
|
||||
defaultMessage: string;
|
||||
},
|
||||
values?: { [key: string]: string },
|
||||
opts?: any,
|
||||
) => {
|
||||
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
|
||||
};
|
||||
|
|
|
|||
15
src/components/hooks/useModified.ts
Normal file
15
src/components/hooks/useModified.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import useStore from 'store/modified';
|
||||
|
||||
export function useModified(key?: string) {
|
||||
const modified = useStore(state => state?.[key]);
|
||||
|
||||
const touch = (id?: string) => {
|
||||
if (id || key) {
|
||||
useStore.setState({ [id || key]: Date.now() });
|
||||
}
|
||||
};
|
||||
|
||||
return { modified, touch };
|
||||
}
|
||||
|
||||
export default useModified;
|
||||
|
|
@ -6,7 +6,7 @@ export function useNavigation(): {
|
|||
pathname: string;
|
||||
query: { [key: string]: string };
|
||||
router: any;
|
||||
makeUrl: (params: any, reset?: boolean) => string;
|
||||
renderUrl: (params: any, reset?: boolean) => string;
|
||||
} {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
|
@ -22,11 +22,11 @@ export function useNavigation(): {
|
|||
return obj;
|
||||
}, [params]);
|
||||
|
||||
function makeUrl(params: any, reset?: boolean) {
|
||||
function renderUrl(params: any, reset?: boolean) {
|
||||
return reset ? pathname : buildUrl(pathname, { ...query, ...params });
|
||||
}
|
||||
|
||||
return { pathname, query, router, makeUrl };
|
||||
return { pathname, query, router, renderUrl };
|
||||
}
|
||||
|
||||
export default useNavigation;
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import useApi from './useApi';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
|
||||
export function useReports(websiteId?: string) {
|
||||
const [modified, setModified] = useState(Date.now());
|
||||
const { get, del, useMutation } = useApi();
|
||||
const { mutate } = useMutation({ mutationFn: (reportId: string) => del(`/reports/${reportId}`) });
|
||||
const queryResult = useFilterQuery({
|
||||
queryKey: ['reports', { websiteId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteReport = (id: any) => {
|
||||
mutate(id, {
|
||||
onSuccess: () => {
|
||||
setModified(Date.now());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...queryResult,
|
||||
deleteReport,
|
||||
};
|
||||
}
|
||||
|
||||
export default useReports;
|
||||
17
src/components/hooks/useTeamUrl.ts
Normal file
17
src/components/hooks/useTeamUrl.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function useTeamUrl(): {
|
||||
teamId?: string;
|
||||
renderTeamUrl: (url: string) => string;
|
||||
} {
|
||||
const pathname = usePathname();
|
||||
const [, teamId] = pathname.match(/^\/teams\/([a-f0-9-]+)/) || [];
|
||||
|
||||
function renderTeamUrl(url: string) {
|
||||
return teamId ? `/teams/${teamId}${url}` : url;
|
||||
}
|
||||
|
||||
return { teamId, renderTeamUrl };
|
||||
}
|
||||
|
||||
export default useTeamUrl;
|
||||
|
|
@ -7,7 +7,7 @@ export function useTimezone() {
|
|||
const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone());
|
||||
|
||||
const saveTimezone = useCallback(
|
||||
value => {
|
||||
(value: string) => {
|
||||
setItem(TIMEZONE_CONFIG, value);
|
||||
setTimezone(value);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import useStore, { setUser } from 'store/app';
|
||||
|
||||
const selector = state => state.user;
|
||||
|
||||
export function useUser() {
|
||||
const user = useStore(selector);
|
||||
|
||||
return { user, setUser };
|
||||
}
|
||||
|
||||
export default useUser;
|
||||
|
|
@ -4,6 +4,7 @@ import Bars from 'assets/bars.svg';
|
|||
import BarChart from 'assets/bar-chart.svg';
|
||||
import Bolt from 'assets/bolt.svg';
|
||||
import Calendar from 'assets/calendar.svg';
|
||||
import Change from 'assets/change.svg';
|
||||
import Clock from 'assets/clock.svg';
|
||||
import Dashboard from 'assets/dashboard.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
|
|
@ -29,6 +30,7 @@ const icons = {
|
|||
BarChart,
|
||||
Bolt,
|
||||
Calendar,
|
||||
Change,
|
||||
Clock,
|
||||
Dashboard,
|
||||
Eye,
|
||||
|
|
|
|||
|
|
@ -2,34 +2,34 @@ import { useState } from 'react';
|
|||
import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
|
||||
import { endOfYear, isSameDay } from 'date-fns';
|
||||
import DatePickerForm from 'components/metrics/DatePickerForm';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useLocale, useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { formatDate } from 'lib/date';
|
||||
import { formatDate, parseDateValue } from 'lib/date';
|
||||
|
||||
export interface DateFilterProps {
|
||||
value: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
offset?: number;
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
selectedUnit?: string;
|
||||
showAllTime?: boolean;
|
||||
alignment?: 'start' | 'center' | 'end';
|
||||
}
|
||||
|
||||
export function DateFilter({
|
||||
value,
|
||||
startDate,
|
||||
endDate,
|
||||
value,
|
||||
offset = 0,
|
||||
className,
|
||||
onChange,
|
||||
selectedUnit,
|
||||
showAllTime = false,
|
||||
alignment = 'end',
|
||||
}: DateFilterProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const { locale } = useLocale();
|
||||
|
||||
const options = [
|
||||
{ label: formatMessage(labels.today), value: '1day' },
|
||||
|
|
@ -76,19 +76,6 @@ export function DateFilter({
|
|||
},
|
||||
].filter(n => n);
|
||||
|
||||
const renderValue = (value: string) => {
|
||||
return value.startsWith('range') ? (
|
||||
<CustomRange
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
selectedUnit={selectedUnit}
|
||||
onClick={() => handleChange('custom')}
|
||||
/>
|
||||
) : (
|
||||
options.find(e => e.value === value).label
|
||||
);
|
||||
};
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
if (value === 'custom') {
|
||||
setShowPicker(true);
|
||||
|
|
@ -104,6 +91,31 @@ export function DateFilter({
|
|||
|
||||
const handleClose = () => setShowPicker(false);
|
||||
|
||||
const renderValue = (value: string) => {
|
||||
const { unit } = parseDateValue(value);
|
||||
|
||||
if (offset && unit === 'year') {
|
||||
return formatDate(startDate, 'yyyy', locale);
|
||||
}
|
||||
|
||||
if (offset && unit === 'month') {
|
||||
return formatDate(startDate, 'MMMM yyyy', locale);
|
||||
}
|
||||
|
||||
if (value.startsWith('range') || offset) {
|
||||
return (
|
||||
<CustomRange
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
unit={unit}
|
||||
onClick={() => handleChange('custom')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return options.find(e => e.value === value).label;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
|
|
@ -137,10 +149,10 @@ export function DateFilter({
|
|||
);
|
||||
}
|
||||
|
||||
const CustomRange = ({ startDate, endDate, selectedUnit, onClick }) => {
|
||||
const CustomRange = ({ startDate, endDate, unit, onClick }) => {
|
||||
const { locale } = useLocale();
|
||||
|
||||
const monthFormat = +selectedUnit?.num === 1 && selectedUnit?.unit === 'month';
|
||||
const monthFormat = unit === 'month';
|
||||
|
||||
function handleClick(e) {
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import { languages } from 'lib/lang';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './LanguageButton.module.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Icon, Icons, TooltipPopup } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function LogoutButton({
|
||||
tooltipPosition = 'top',
|
||||
|
|
@ -9,7 +9,7 @@ export function LogoutButton({
|
|||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
return (
|
||||
<Link href="/src/app/logout/logout">
|
||||
<Link href="/src/app/logout/LogoutPage">
|
||||
<TooltipPopup label={formatMessage(labels.logout)} position={tooltipPosition}>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
.menu {
|
||||
width: 200px;
|
||||
z-index: var(--z-index-popup);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.item {
|
||||
|
|
@ -16,3 +20,9 @@
|
|||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--font-color200);
|
||||
background: var(--base75);
|
||||
padding: var(--size300) var(--size600);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { Key } from 'react';
|
||||
import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import { useMessages, useLogin, useLocale } from 'components/hooks';
|
||||
import { CURRENT_VERSION } from 'lib/constants';
|
||||
import styles from './ProfileButton.module.css';
|
||||
|
||||
export function ProfileButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { user } = useLogin();
|
||||
const router = useRouter();
|
||||
const { dir } = useLocale();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
|
||||
const handleSelect = key => {
|
||||
const handleSelect = (key: Key, close: () => void) => {
|
||||
if (key === 'profile') {
|
||||
router.push('/settings/profile');
|
||||
router.push('/profile');
|
||||
}
|
||||
if (key === 'logout') {
|
||||
router.push('/logout');
|
||||
}
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -29,31 +29,28 @@ export function ProfileButton() {
|
|||
<Icon>
|
||||
<Icons.Profile />
|
||||
</Icon>
|
||||
<Icon size="sm">
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
|
||||
<Menu variant="popup" onSelect={handleSelect} className={styles.menu}>
|
||||
<Item key="user" className={styles.item}>
|
||||
<Text>{user.username}</Text>
|
||||
</Item>
|
||||
<Item key="profile" className={styles.item} divider={true}>
|
||||
<Icon>
|
||||
<Icons.User />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.profile)}</Text>
|
||||
</Item>
|
||||
{!cloudMode && (
|
||||
<Item key="logout" className={styles.item}>
|
||||
{(close: () => void) => (
|
||||
<Menu onSelect={key => handleSelect(key, close)} className={styles.menu}>
|
||||
<Text className={styles.name}>{user.username}</Text>
|
||||
<Item key="profile" className={styles.item} divider={true}>
|
||||
<Icon>
|
||||
<Icons.Logout />
|
||||
<Icons.User />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.logout)}</Text>
|
||||
<Text>{formatMessage(labels.profile)}</Text>
|
||||
</Item>
|
||||
)}
|
||||
<div className={styles.version}>{`v${CURRENT_VERSION}`}</div>
|
||||
</Menu>
|
||||
{!cloudMode && (
|
||||
<Item key="logout" className={styles.item}>
|
||||
<Icon>
|
||||
<Icons.Logout />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.logout)}</Text>
|
||||
</Item>
|
||||
)}
|
||||
<div className={styles.version}>{`v${CURRENT_VERSION}`}</div>
|
||||
</Menu>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { LoadingButton, Icon, TooltipPopup } from 'react-basics';
|
||||
import { setWebsiteDateRange } from 'store/websites';
|
||||
import useDateRange from 'components/hooks/useDateRange';
|
||||
import { useDateRange } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function RefreshButton({
|
||||
websiteId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Button, Icon, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
|
||||
import TimezoneSetting from 'app/(main)/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'app/(main)/settings/profile/DateRangeSetting';
|
||||
import TimezoneSetting from 'app/(main)/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'app/(main)/profile/DateRangeSetting';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './SettingsButton.module.css';
|
||||
|
||||
export function SettingsButton() {
|
||||
|
|
|
|||
20
src/components/input/TeamsButton.module.css
Normal file
20
src/components/input/TeamsButton.module.css
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.button {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: var(--base600);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 8px 16px;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
60
src/components/input/TeamsButton.tsx
Normal file
60
src/components/input/TeamsButton.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Key } from 'react';
|
||||
import { Text, Icon, Button, Popup, Menu, Item, PopupTrigger, Flexbox } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import Icons from 'components/icons';
|
||||
import { useLogin, useMessages, useNavigation } from 'components/hooks';
|
||||
import styles from './TeamsButton.module.css';
|
||||
|
||||
export function TeamsButton({ teamId }: { teamId: string }) {
|
||||
const { user } = useLogin();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { router } = useNavigation();
|
||||
const team = user?.teams?.find(({ id }) => id === teamId);
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
const handleSelect = (close: () => void, id: Key) => {
|
||||
if (id !== user.id) {
|
||||
router.push(cloudMode ? `${process.env.cloudUrl}/teams/${id}` : `/teams/${id}`);
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button className={styles.button} variant="quiet">
|
||||
<Icon>{teamId ? <Icons.Users /> : <Icons.User />}</Icon>
|
||||
<Text>{teamId ? team?.name : user.username}</Text>
|
||||
</Button>
|
||||
<Popup alignment="end">
|
||||
{(close: () => void) => (
|
||||
<Menu variant="popup" onSelect={handleSelect.bind(null, close)}>
|
||||
<div className={styles.heading}>{formatMessage(labels.myAccount)}</div>
|
||||
<Item key={user.id} className={classNames({ [styles.selected]: !teamId })}>
|
||||
<Flexbox gap={10} alignItems="center">
|
||||
<Icon>
|
||||
<Icons.User />
|
||||
</Icon>
|
||||
<Text>{user.username}</Text>
|
||||
</Flexbox>
|
||||
</Item>
|
||||
<div className={styles.heading}>{formatMessage(labels.team)}</div>
|
||||
{user?.teams?.map(({ id, name }) => (
|
||||
<Item key={id} className={classNames({ [styles.selected]: id === teamId })}>
|
||||
<Flexbox gap={10} alignItems="center">
|
||||
<Icon>
|
||||
<Icons.Users />
|
||||
</Icon>
|
||||
<Text>{name}</Text>
|
||||
</Flexbox>
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsButton;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useTransition, animated } from '@react-spring/web';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import { useTheme } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './ThemeButton.module.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,35 @@
|
|||
import useDateRange from 'components/hooks/useDateRange';
|
||||
import { useDateRange } from 'components/hooks';
|
||||
import { isAfter } from 'date-fns';
|
||||
import { incrementDateRange } from 'lib/date';
|
||||
import { getOffsetDateRange } from 'lib/date';
|
||||
import { Button, Icon, Icons } from 'react-basics';
|
||||
import DateFilter from './DateFilter';
|
||||
import styles from './WebsiteDateFilter.module.css';
|
||||
import { DateRange } from 'lib/types';
|
||||
|
||||
export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { value, startDate, endDate, selectedUnit } = dateRange;
|
||||
const isFutureDate =
|
||||
value !== 'all' &&
|
||||
selectedUnit &&
|
||||
isAfter(incrementDateRange(dateRange, -1).startDate, new Date());
|
||||
const { value, startDate, endDate, offset } = dateRange;
|
||||
const disableForward =
|
||||
value === 'all' || isAfter(getOffsetDateRange(dateRange, 1).startDate, new Date());
|
||||
|
||||
const handleChange = value => {
|
||||
const handleChange = (value: string | DateRange) => {
|
||||
setDateRange(value);
|
||||
};
|
||||
|
||||
const handleIncrement = value => {
|
||||
setDateRange(incrementDateRange(dateRange, value));
|
||||
const handleIncrement = (increment: number) => {
|
||||
setDateRange(getOffsetDateRange(dateRange, increment));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{value !== 'all' && selectedUnit && (
|
||||
{value !== 'all' && (
|
||||
<div className={styles.buttons}>
|
||||
<Button onClick={() => handleIncrement(1)}>
|
||||
<Button onClick={() => handleIncrement(-1)}>
|
||||
<Icon rotate={90}>
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button onClick={() => handleIncrement(-1)} disabled={isFutureDate}>
|
||||
<Button onClick={() => handleIncrement(1)} disabled={disableForward}>
|
||||
<Icon rotate={270}>
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
|
|
@ -42,7 +41,7 @@ export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
|
|||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
selectedUnit={selectedUnit}
|
||||
offset={offset}
|
||||
onChange={handleChange}
|
||||
showAllTime={true}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,58 @@
|
|||
import { useState, Key } from 'react';
|
||||
import { Dropdown, Item } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useWebsite, useWebsites, useMessages } from 'components/hooks';
|
||||
import Empty from 'components/common/Empty';
|
||||
import styles from './WebsiteSelect.module.css';
|
||||
|
||||
export function WebsiteSelect({
|
||||
websiteId,
|
||||
teamId,
|
||||
userId,
|
||||
onSelect,
|
||||
}: {
|
||||
websiteId: string;
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
userId?: string;
|
||||
onSelect?: (key: any) => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data } = useQuery({
|
||||
queryKey: ['websites:me'],
|
||||
queryFn: () => get('/me/websites', { pageSize: 100 }),
|
||||
});
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [query, setQuery] = useState('');
|
||||
const [selectedId, setSelectedId] = useState<Key>(websiteId);
|
||||
|
||||
const renderValue = value => {
|
||||
return data?.data?.find(({ id }) => id === value)?.name;
|
||||
const { data: website } = useWebsite(selectedId as string);
|
||||
|
||||
const queryResult = useWebsites({ teamId, userId }, { query, pageSize: 5 });
|
||||
|
||||
const renderValue = () => {
|
||||
return website?.name;
|
||||
};
|
||||
|
||||
const renderEmpty = () => {
|
||||
return <Empty message={formatMessage(messages.noResultsFound)} />;
|
||||
};
|
||||
|
||||
const handleSelect = (value: any) => {
|
||||
setSelectedId(value);
|
||||
onSelect?.(value);
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setQuery(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menuProps={{ className: styles.dropdown }}
|
||||
items={data?.data}
|
||||
value={websiteId}
|
||||
items={queryResult?.result?.data as any[]}
|
||||
value={selectedId as string}
|
||||
renderValue={renderValue}
|
||||
onChange={onSelect}
|
||||
renderEmpty={renderEmpty}
|
||||
onChange={handleSelect}
|
||||
alignment="end"
|
||||
placeholder={formatMessage(labels.selectWebsite)}
|
||||
allowSearch={true}
|
||||
onSearch={handleSearch}
|
||||
isLoading={queryResult.query.isLoading}
|
||||
>
|
||||
{({ id, name }) => <Item key={id}>{name}</Item>}
|
||||
</Dropdown>
|
||||
|
|
|
|||
31
src/components/layout/MenuLayout.module.css
Normal file
31
src/components/layout/MenuLayout.module.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 240px;
|
||||
padding-top: 34px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
24
src/components/layout/MenuLayout.tsx
Normal file
24
src/components/layout/MenuLayout.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import styles from './MenuLayout.module.css';
|
||||
|
||||
export function MenuLayout({ items = [], children }: { items: any[]; children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
const getKey = () => items.find(({ url }) => pathname === url)?.key;
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
{!cloudMode && (
|
||||
<div className={styles.menu}>
|
||||
<SideNav items={items} shallow={true} selectedKey={getKey()} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuLayout;
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Banner, Loading } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './Page.module.css';
|
||||
|
||||
export function Page({
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--base700);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Icon } from 'react-basics';
|
||||
import styles from './PageHeader.module.css';
|
||||
|
||||
export interface PageHeaderProps {
|
||||
export function PageHeader({
|
||||
title,
|
||||
icon,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
title?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, className, children }: PageHeaderProps) {
|
||||
}) {
|
||||
return (
|
||||
<div className={classNames(styles.header, className)}>
|
||||
{icon && (
|
||||
<Icon size="lg" className={styles.icon}>
|
||||
{icon}
|
||||
</Icon>
|
||||
)}
|
||||
{title && <div className={styles.title}>{title}</div>}
|
||||
<div className={styles.actions}>{children}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.item a {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
export const labels = defineMessages({
|
||||
ok: { id: 'label.ok', defaultMessage: 'OK' },
|
||||
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
|
||||
required: { id: 'label.required', defaultMessage: 'Required' },
|
||||
save: { id: 'label.save', defaultMessage: 'Save' },
|
||||
|
|
@ -16,7 +17,8 @@ export const labels = defineMessages({
|
|||
role: { id: 'label.role', defaultMessage: 'Role' },
|
||||
user: { id: 'label.user', defaultMessage: 'User' },
|
||||
viewOnly: { id: 'label.view-only', defaultMessage: 'View only' },
|
||||
admin: { id: 'label.admin', defaultMessage: 'Administrator' },
|
||||
manage: { id: 'label.manage', defaultMessage: 'Manage' },
|
||||
administrator: { id: 'label.administrator', defaultMessage: 'Administrator' },
|
||||
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
|
||||
details: { id: 'label.details', defaultMessage: 'Details' },
|
||||
website: { id: 'label.website', defaultMessage: 'Website' },
|
||||
|
|
@ -26,6 +28,7 @@ export const labels = defineMessages({
|
|||
created: { id: 'label.created', defaultMessage: 'Created' },
|
||||
edit: { id: 'label.edit', defaultMessage: 'Edit' },
|
||||
name: { id: 'label.name', defaultMessage: 'Name' },
|
||||
member: { id: 'label.member', defaultMessage: 'Member' },
|
||||
members: { id: 'label.members', defaultMessage: 'Members' },
|
||||
accessCode: { id: 'label.access-code', defaultMessage: 'Access code' },
|
||||
teamId: { id: 'label.team-id', defaultMessage: 'Team ID' },
|
||||
|
|
@ -40,7 +43,7 @@ export const labels = defineMessages({
|
|||
owner: { id: 'label.owner', defaultMessage: 'Owner' },
|
||||
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
|
||||
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
|
||||
teamGuest: { id: 'label.team-guest', defaultMessage: 'Team guest' },
|
||||
teamViewOnly: { id: 'label.team-view-only', defaultMessage: 'Team view only' },
|
||||
enableShareUrl: { id: 'label.enable-share-url', defaultMessage: 'Enable share URL' },
|
||||
data: { id: 'label.data', defaultMessage: 'Data' },
|
||||
trackingCode: { id: 'label.tracking-code', defaultMessage: 'Tracking code' },
|
||||
|
|
@ -50,8 +53,13 @@ export const labels = defineMessages({
|
|||
websiteId: { id: 'label.website-id', defaultMessage: 'Website ID' },
|
||||
resetWebsite: { id: 'label.reset-website', defaultMessage: 'Reset website' },
|
||||
deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' },
|
||||
transferWebsite: { id: 'label.transfer-website', defaultMessage: 'Transfer website' },
|
||||
deleteReport: { id: 'label.delete-report', defaultMessage: 'Delete report' },
|
||||
reset: { id: 'label.reset', defaultMessage: 'Reset' },
|
||||
addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' },
|
||||
addMember: { id: 'label.add-member', defaultMessage: 'Add member' },
|
||||
editMember: { id: 'label.edit-member', defaultMessage: 'Edit member' },
|
||||
removeMember: { id: 'label.remove-member', defaultMessage: 'Remove member' },
|
||||
addDescription: { id: 'label.add-description', defaultMessage: 'Add description' },
|
||||
changePassword: { id: 'label.change-password', defaultMessage: 'Change password' },
|
||||
currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' },
|
||||
|
|
@ -105,6 +113,7 @@ export const labels = defineMessages({
|
|||
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
|
||||
customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
|
||||
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
|
||||
selectRole: { id: 'label.select-role', defaultMessage: 'Select role' },
|
||||
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
|
||||
all: { id: 'label.all', defaultMessage: 'All' },
|
||||
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
|
||||
|
|
@ -197,6 +206,9 @@ export const labels = defineMessages({
|
|||
id: 'label.number-of-records',
|
||||
defaultMessage: '{x} {x, plural, one {record} other {records}}',
|
||||
},
|
||||
select: { id: 'label.select', defaultMessage: 'Select' },
|
||||
myAccount: { id: 'label.my-account', defaultMessage: 'My account' },
|
||||
transfer: { id: 'label.transfer', defaultMessage: 'Transfer' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
@ -213,6 +225,10 @@ export const messages = defineMessages({
|
|||
id: 'message.confirm-delete',
|
||||
defaultMessage: 'Are you sure you want to delete {target}?',
|
||||
},
|
||||
confirmRemove: {
|
||||
id: 'message.confirm-remove',
|
||||
defaultMessage: 'Are you sure you want to remove {target}?',
|
||||
},
|
||||
confirmLeave: {
|
||||
id: 'message.confirm-leave',
|
||||
defaultMessage: 'Are you sure you want to leave {target}?',
|
||||
|
|
@ -227,7 +243,7 @@ export const messages = defineMessages({
|
|||
},
|
||||
shareUrl: {
|
||||
id: 'message.share-url',
|
||||
defaultMessage: 'Your website stats are publically available at the following URL:',
|
||||
defaultMessage: 'Your website stats are publicly available at the following URL:',
|
||||
},
|
||||
trackingCode: {
|
||||
id: 'message.tracking-code',
|
||||
|
|
@ -238,13 +254,9 @@ export const messages = defineMessages({
|
|||
id: 'message.team-already-member',
|
||||
defaultMessage: 'You are already a member of the team.',
|
||||
},
|
||||
deleteAccount: {
|
||||
id: 'message.delete-account',
|
||||
defaultMessage: 'To delete this account, type {confirmation} in the box below to confirm.',
|
||||
},
|
||||
deleteWebsite: {
|
||||
id: 'message.delete-website',
|
||||
defaultMessage: 'To delete this website, type {confirmation} in the box below to confirm.',
|
||||
actionConfirmation: {
|
||||
id: 'message.action-confirmation',
|
||||
defaultMessage: 'Type {confirmation} in the box below to confirm.',
|
||||
},
|
||||
resetWebsite: {
|
||||
id: 'message.reset-website',
|
||||
|
|
@ -263,6 +275,10 @@ export const messages = defineMessages({
|
|||
id: 'message.delete-website-warning',
|
||||
defaultMessage: 'All website data will be deleted.',
|
||||
},
|
||||
deleteTeamWarning: {
|
||||
id: 'message.delete-team-warning',
|
||||
defaultMessage: 'Deleting a team will also delete all team websites.',
|
||||
},
|
||||
noResultsFound: {
|
||||
id: 'message.no-results-found',
|
||||
defaultMessage: 'No results found.',
|
||||
|
|
@ -312,4 +328,16 @@ export const messages = defineMessages({
|
|||
id: 'message.new-version-available',
|
||||
defaultMessage: 'A new version of Umami {version} is available!',
|
||||
},
|
||||
transferWebsite: {
|
||||
id: 'message.transfer-website',
|
||||
defaultMessage: 'Transfer website ownership to your account or another team.',
|
||||
},
|
||||
transferTeamWebsiteToUser: {
|
||||
id: 'message.transfer-team-website-to-user',
|
||||
defaultMessage: 'Transfer this website to your account?',
|
||||
},
|
||||
transferUserWebsiteToTeam: {
|
||||
id: 'message.transfer-user-website-to-team',
|
||||
defaultMessage: 'Select the team to transfer this website to.',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import { StatusLight } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useApi } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './ActiveUsers.module.css';
|
||||
|
||||
export function ActiveUsers({
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import classNames from 'classnames';
|
|||
import Chart from 'chart.js/auto';
|
||||
import HoverTooltip from 'components/common/HoverTooltip';
|
||||
import Legend from 'components/metrics/Legend';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useTheme } from 'components/hooks';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import { renderNumberLabels } from 'lib/charts';
|
||||
import styles from './BarChart.module.css';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import FilterLink from 'components/common/FilterLink';
|
||||
import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useFormat from 'components/hooks/useFormat';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useFormat } from 'components/hooks';
|
||||
|
||||
export function BrowsersTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import { emptyFilter } from 'lib/filters';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useCountryNames from 'components/hooks/useCountryNames';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useCountryNames } from 'components/hooks';
|
||||
|
||||
export function CitiesTable(props: MetricsTableProps) {
|
||||
const { locale } = useLocale();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import FilterLink from 'components/common/FilterLink';
|
||||
import useCountryNames from 'components/hooks/useCountryNames';
|
||||
import { useCountryNames } from 'components/hooks';
|
||||
import { useLocale, useMessages, useFormat } from 'components/hooks';
|
||||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, ButtonGroup, Calendar } from 'react-basics';
|
||||
import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { FILTER_DAY, FILTER_RANGE } from 'lib/constants';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './DatePickerForm.module.css';
|
||||
|
||||
export function DatePickerForm({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useFormat } from 'components/hooks';
|
||||
|
||||
export function DevicesTable(props: MetricsTableProps) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ import { Loading } from 'react-basics';
|
|||
import { colord } from 'colord';
|
||||
import BarChart from './BarChart';
|
||||
import { getDateArray } from 'lib/date';
|
||||
import { useApi, useLocale, useDateRange, useTimezone, useNavigation } from 'components/hooks';
|
||||
import {
|
||||
useLocale,
|
||||
useDateRange,
|
||||
useTimezone,
|
||||
useNavigation,
|
||||
useWebsiteEvents,
|
||||
} from 'components/hooks';
|
||||
import { EVENT_COLORS } from 'lib/constants';
|
||||
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
|
||||
|
||||
|
|
@ -14,34 +20,29 @@ export interface EventsChartProps {
|
|||
}
|
||||
|
||||
export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
||||
const { get, useQuery } = useApi();
|
||||
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
|
||||
const [{ startDate, endDate, unit, offset }] = useDateRange(websiteId);
|
||||
const { locale } = useLocale();
|
||||
const [timezone] = useTimezone();
|
||||
const {
|
||||
query: { url, event },
|
||||
} = useNavigation();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['events', websiteId, modified, event],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/events`, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
unit,
|
||||
timezone,
|
||||
url,
|
||||
event,
|
||||
token,
|
||||
}),
|
||||
enabled: !!websiteId,
|
||||
const { data, isLoading } = useWebsiteEvents(websiteId, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
unit,
|
||||
timezone,
|
||||
url,
|
||||
event,
|
||||
token,
|
||||
offset,
|
||||
});
|
||||
|
||||
const datasets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
if (isLoading) return data;
|
||||
|
||||
const map = data.reduce((obj, { x, t, y }) => {
|
||||
const map = (data as any[]).reduce((obj, { x, t, y }) => {
|
||||
if (!obj[x]) {
|
||||
obj[x] = [];
|
||||
}
|
||||
|
|
@ -75,12 +76,12 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
|||
return (
|
||||
<BarChart
|
||||
className={className}
|
||||
datasets={datasets}
|
||||
datasets={datasets as any[]}
|
||||
unit={unit}
|
||||
loading={isLoading}
|
||||
stacked
|
||||
stacked={true}
|
||||
renderXLabel={renderDateLabels(unit, locale)}
|
||||
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function EventsTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { safeDecodeURI } from 'next-basics';
|
||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import useNavigation from 'components/hooks/useNavigation';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useFormat from 'components/hooks/useFormat';
|
||||
import { useNavigation } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useFormat } from 'components/hooks';
|
||||
import styles from './FilterTags.module.css';
|
||||
|
||||
export function FilterTags({ params }) {
|
||||
|
|
@ -10,7 +10,7 @@ export function FilterTags({ params }) {
|
|||
const { formatValue } = useFormat();
|
||||
const {
|
||||
router,
|
||||
makeUrl,
|
||||
renderUrl,
|
||||
query: { view },
|
||||
} = useNavigation();
|
||||
|
||||
|
|
@ -19,11 +19,11 @@ export function FilterTags({ params }) {
|
|||
}
|
||||
|
||||
function handleCloseFilter(param?: string) {
|
||||
router.push(makeUrl({ [param]: undefined }));
|
||||
router.push(renderUrl({ [param]: undefined }));
|
||||
}
|
||||
|
||||
function handleResetFilter() {
|
||||
router.push(makeUrl({ view }, true));
|
||||
router.push(renderUrl({ view }, true));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import useLanguageNames from 'components/hooks/useLanguageNames';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useLanguageNames } from 'components/hooks';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function LanguagesTable({
|
||||
onDataLoad,
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import { useEffect } from 'react';
|
|||
import { StatusLight } from 'react-basics';
|
||||
import { colord } from 'colord';
|
||||
import classNames from 'classnames';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useForceUpdate from 'components/hooks/useForceUpdate';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useForceUpdate } from 'components/hooks';
|
||||
import styles from './Legend.module.css';
|
||||
|
||||
export function Legend({ chart }) {
|
||||
const { locale } = useLocale();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const handleClick = index => {
|
||||
const handleClick = (index: string | number) => {
|
||||
const meta = chart.getDatasetMeta(index);
|
||||
|
||||
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useSpring, animated, config } from '@react-spring/web';
|
|||
import classNames from 'classnames';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './ListTable.module.css';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
|
|
@ -63,6 +63,7 @@ export function ListTable({
|
|||
{data?.length === 0 && <Empty className={styles.empty} />}
|
||||
{virtualize && data.length > 0 ? (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={itemCount * ITEM_SIZE}
|
||||
itemCount={data.length}
|
||||
itemSize={ITEM_SIZE}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { Loading, Icon, Text, SearchField } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import useDateRange from 'components/hooks/useDateRange';
|
||||
import useNavigation from 'components/hooks/useNavigation';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import ListTable, { ListTableProps } from './ListTable';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import {
|
||||
useDateRange,
|
||||
useNavigation,
|
||||
useWebsiteMetrics,
|
||||
useMessages,
|
||||
useLocale,
|
||||
useFormat,
|
||||
} from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useFormat from 'components//hooks/useFormat';
|
||||
import ListTable, { ListTableProps } from './ListTable';
|
||||
import styles from './MetricsTable.module.css';
|
||||
|
||||
export interface MetricsTableProps extends ListTableProps {
|
||||
|
|
@ -43,55 +45,36 @@ export function MetricsTable({
|
|||
}: MetricsTableProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatValue } = useFormat();
|
||||
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
|
||||
const [{ startDate, endDate }] = useDateRange(websiteId);
|
||||
const {
|
||||
makeUrl,
|
||||
renderUrl,
|
||||
query: { url, referrer, title, os, browser, device, country, region, city },
|
||||
} = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const { dir } = useLocale();
|
||||
const { data, isLoading, isFetched, error } = useQuery({
|
||||
queryKey: [
|
||||
'websites:metrics',
|
||||
{
|
||||
websiteId,
|
||||
type,
|
||||
modified,
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
title,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
const filters = { url, title, referrer, os, browser, device, country, region, city };
|
||||
|
||||
filters[type] = undefined;
|
||||
|
||||
const data = await get(`/websites/${websiteId}/metrics`, {
|
||||
type,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
limit,
|
||||
...filters,
|
||||
});
|
||||
|
||||
onDataLoad?.(data);
|
||||
|
||||
return data;
|
||||
const { data, isLoading, isFetched, error } = useWebsiteMetrics(
|
||||
websiteId,
|
||||
{
|
||||
type,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
title,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
},
|
||||
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
|
||||
});
|
||||
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
|
||||
);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (data) {
|
||||
let items: any[] = data;
|
||||
let items = data as any[];
|
||||
|
||||
if (dataFilter) {
|
||||
if (Array.isArray(dataFilter)) {
|
||||
|
|
@ -142,7 +125,7 @@ export function MetricsTable({
|
|||
{!data && isLoading && !isFetched && <Loading icon="dots" />}
|
||||
<div className={styles.footer}>
|
||||
{data && !error && limit && (
|
||||
<LinkButton href={makeUrl({ view: type })} variant="quiet">
|
||||
<LinkButton href={renderUrl({ view: type })} variant="quiet">
|
||||
<Text>{formatMessage(labels.more)}</Text>
|
||||
<Icon size="sm" rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.ArrowRight />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useFormat from 'components/hooks/useFormat';
|
||||
import { useMessages, useFormat } from 'components/hooks';
|
||||
|
||||
export function OSTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import FilterLink from 'components/common/FilterLink';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useNavigation from 'components/hooks/useNavigation';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useNavigation } from 'components/hooks';
|
||||
import { emptyFilter } from 'lib/filters';
|
||||
|
||||
export interface PagesTableProps extends MetricsTableProps {
|
||||
|
|
@ -12,13 +12,13 @@ export interface PagesTableProps extends MetricsTableProps {
|
|||
export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProps) {
|
||||
const {
|
||||
router,
|
||||
makeUrl,
|
||||
renderUrl,
|
||||
query: { view = 'url' },
|
||||
} = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSelect = (key: any) => {
|
||||
router.push(makeUrl({ view: key }), { scroll: true });
|
||||
router.push(renderUrl({ view: key }), { scroll: true });
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import FilterButtons from 'components/common/FilterButtons';
|
|||
import { emptyFilter, paramFilter } from 'lib/filters';
|
||||
import { FILTER_RAW, FILTER_COMBINED } from 'lib/constants';
|
||||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './QueryParametersTable.module.css';
|
||||
|
||||
const filters = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function ReferrersTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import FilterLink from 'components/common/FilterLink';
|
||||
import { emptyFilter } from 'lib/filters';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useCountryNames from 'components/hooks/useCountryNames';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useCountryNames } from 'components/hooks';
|
||||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import regions from 'public/iso-3166-2.json';
|
||||
import regions from '../../../public/iso-3166-2.json';
|
||||
|
||||
export function RegionsTable(props: MetricsTableProps) {
|
||||
const { locale } = useLocale();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function ScreenTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import classNames from 'classnames';
|
|||
import { colord } from 'colord';
|
||||
import HoverTooltip from 'components/common/HoverTooltip';
|
||||
import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import useCountryNames from 'components/hooks/useCountryNames';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useTheme } from 'components/hooks';
|
||||
import { useCountryNames } from 'components/hooks';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import styles from './WorldMap.module.css';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue