mirror of
https://github.com/kremalicious/umami.git
synced 2025-01-02 10:13:11 +01:00
Custom date range selection.
This commit is contained in:
parent
7a8ab94bba
commit
4e103152b2
@ -13,6 +13,8 @@ export default function Button({
|
||||
className,
|
||||
tooltip,
|
||||
tooltipId,
|
||||
disabled = false,
|
||||
onClick = () => {},
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
@ -27,7 +29,10 @@ export default function Button({
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
[styles.action]: variant === 'action',
|
||||
[styles.danger]: variant === 'danger',
|
||||
[styles.disabled]: disabled,
|
||||
})}
|
||||
disabled={disabled}
|
||||
onClick={!disabled ? onClick : null}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon icon={icon} size={size} />}
|
||||
|
@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #eaeaea;
|
||||
background: var(--gray200);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
@ -38,19 +38,32 @@
|
||||
}
|
||||
|
||||
.action {
|
||||
color: var(--gray50) !important;
|
||||
background: var(--gray900) !important;
|
||||
color: var(--gray50);
|
||||
background: var(--gray900);
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
background: var(--gray800) !important;
|
||||
background: var(--gray800);
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--gray50) !important;
|
||||
background: var(--red500) !important;
|
||||
color: var(--gray50);
|
||||
background: var(--red500);
|
||||
}
|
||||
|
||||
.danger:hover {
|
||||
background: var(--red400) !important;
|
||||
background: var(--red400);
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: var(--gray500);
|
||||
background: var(--gray75);
|
||||
}
|
||||
|
||||
.button:disabled:active {
|
||||
color: var(--gray500);
|
||||
}
|
||||
|
||||
.button:disabled:hover {
|
||||
background: var(--gray75);
|
||||
}
|
||||
|
258
components/common/Calendar.js
Normal file
258
components/common/Calendar.js
Normal file
@ -0,0 +1,258 @@
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
startOfWeek,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
endOfMonth,
|
||||
addDays,
|
||||
subDays,
|
||||
addYears,
|
||||
subYears,
|
||||
addMonths,
|
||||
setMonth,
|
||||
setYear,
|
||||
isSameDay,
|
||||
isBefore,
|
||||
isAfter,
|
||||
} from 'date-fns';
|
||||
import Button from './Button';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
import { chunk } from 'lib/array';
|
||||
import Chevron from 'assets/chevron-down.svg';
|
||||
import Cross from 'assets/times.svg';
|
||||
import styles from './Calendar.module.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default function Calendar({ date, minDate, maxDate, onChange }) {
|
||||
const [locale] = useLocale();
|
||||
const [selectMonth, setSelectMonth] = useState(false);
|
||||
const [selectYear, setSelectYear] = useState(false);
|
||||
|
||||
const month = dateFormat(date, 'MMMM', locale);
|
||||
const year = date.getFullYear();
|
||||
|
||||
function toggleMonthSelect() {
|
||||
setSelectYear(false);
|
||||
setSelectMonth(state => !state);
|
||||
}
|
||||
|
||||
function toggleYearSelect() {
|
||||
setSelectMonth(false);
|
||||
setSelectYear(state => !state);
|
||||
}
|
||||
|
||||
function handleChange(value) {
|
||||
setSelectMonth(false);
|
||||
setSelectYear(false);
|
||||
if (value) {
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.calendar}>
|
||||
<div className={styles.header}>
|
||||
<div>{date.getDate()}</div>
|
||||
<div
|
||||
className={classNames(styles.selector, { [styles.open]: selectMonth })}
|
||||
onClick={toggleMonthSelect}
|
||||
>
|
||||
{month}
|
||||
<Icon className={styles.icon} icon={selectMonth ? <Cross /> : <Chevron />} size="small" />
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles.selector, { [styles.open]: selectYear })}
|
||||
onClick={toggleYearSelect}
|
||||
>
|
||||
{year}
|
||||
<Icon className={styles.icon} icon={selectYear ? <Cross /> : <Chevron />} size="small" />
|
||||
</div>
|
||||
</div>
|
||||
{!selectMonth && !selectYear && (
|
||||
<DaySelector
|
||||
date={date}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
locale={locale}
|
||||
onSelect={handleChange}
|
||||
/>
|
||||
)}
|
||||
{selectMonth && (
|
||||
<MonthSelector
|
||||
date={date}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
locale={locale}
|
||||
onSelect={handleChange}
|
||||
onClose={toggleMonthSelect}
|
||||
/>
|
||||
)}
|
||||
{selectYear && (
|
||||
<YearSelector
|
||||
date={date}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
onSelect={handleChange}
|
||||
onClose={toggleYearSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
||||
const startWeek = startOfWeek(date);
|
||||
const startMonth = startOfMonth(date);
|
||||
const startDay = subDays(startMonth, startMonth.getDay() + 1);
|
||||
const month = date.getMonth();
|
||||
const year = date.getFullYear();
|
||||
|
||||
const daysOfWeek = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
daysOfWeek.push(addDays(startWeek, i));
|
||||
}
|
||||
|
||||
const days = [];
|
||||
for (let i = 0; i < 35; i++) {
|
||||
days.push(addDays(startDay, i));
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{daysOfWeek.map((day, i) => (
|
||||
<th key={i} className={locale}>
|
||||
{dateFormat(day, 'EEE', locale)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chunk(days, 7).map((week, i) => (
|
||||
<tr key={i}>
|
||||
{week.map((day, j) => {
|
||||
const disabled = isBefore(day, minDate) || isAfter(day, maxDate);
|
||||
return (
|
||||
<td
|
||||
key={j}
|
||||
className={classNames({
|
||||
[styles.selected]: isSameDay(date, day),
|
||||
[styles.faded]: day.getMonth() !== month || day.getFullYear() !== year,
|
||||
[styles.disabled]: disabled,
|
||||
})}
|
||||
onClick={!disabled ? () => onSelect(day) : null}
|
||||
>
|
||||
{day.getDate()}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
||||
const start = startOfYear(date);
|
||||
const months = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
months.push(endOfMonth(addMonths(start, i)));
|
||||
}
|
||||
|
||||
function handleSelect(value) {
|
||||
onSelect(setMonth(date, value));
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
{chunk(months, 3).map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((month, j) => {
|
||||
const disabled = isBefore(month, minDate) || isAfter(month, maxDate);
|
||||
return (
|
||||
<td
|
||||
key={j}
|
||||
className={classNames(locale, {
|
||||
[styles.selected]: month.getMonth() === date.getMonth(),
|
||||
[styles.disabled]: disabled,
|
||||
})}
|
||||
onClick={!disabled ? () => handleSelect(month.getMonth()) : null}
|
||||
>
|
||||
{dateFormat(month, 'MMMM', locale)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
|
||||
const [currentDate, setCurrentDate] = useState(date);
|
||||
const year = date.getFullYear();
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const minYear = minDate.getFullYear();
|
||||
const maxYear = maxDate.getFullYear();
|
||||
const years = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
years.push(currentYear - 7 + i);
|
||||
}
|
||||
|
||||
function handleSelect(value) {
|
||||
onSelect(setYear(date, value));
|
||||
}
|
||||
|
||||
function handlePrevClick() {
|
||||
setCurrentDate(state => subYears(state, 15));
|
||||
}
|
||||
|
||||
function handleNextClick() {
|
||||
setCurrentDate(state => addYears(state, 15));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.pager}>
|
||||
<Button
|
||||
icon={<Chevron />}
|
||||
size="xsmall"
|
||||
className={styles.left}
|
||||
onClick={handlePrevClick}
|
||||
disabled={years[0] <= minYear}
|
||||
/>
|
||||
<table>
|
||||
<tbody>
|
||||
{chunk(years, 5).map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((n, j) => (
|
||||
<td
|
||||
key={j}
|
||||
className={classNames({
|
||||
[styles.selected]: n === year,
|
||||
[styles.disabled]: n < minYear || n > maxYear,
|
||||
})}
|
||||
onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))}
|
||||
>
|
||||
{n}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Button
|
||||
icon={<Chevron />}
|
||||
size="xsmall"
|
||||
className={styles.right}
|
||||
onClick={handleNextClick}
|
||||
disabled={years[years.length - 1] > maxYear}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
84
components/common/Calendar.module.css
Normal file
84
components/common/Calendar.module.css
Normal file
@ -0,0 +1,84 @@
|
||||
.calendar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: var(--font-size-small);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar table {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar td {
|
||||
color: var(--gray800);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.calendar td:hover {
|
||||
background: var(--gray100);
|
||||
}
|
||||
|
||||
.calendar td.faded {
|
||||
color: var(--gray500);
|
||||
}
|
||||
|
||||
.calendar td.selected {
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--gray600);
|
||||
}
|
||||
|
||||
.calendar td.selected:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.calendar td.disabled {
|
||||
color: var(--gray300);
|
||||
background: var(--gray75);
|
||||
}
|
||||
|
||||
.calendar td.disabled:hover {
|
||||
cursor: default;
|
||||
background: var(--gray75);
|
||||
}
|
||||
|
||||
.calendar td.faded.disabled {
|
||||
color: var(--gray200);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
line-height: 40px;
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
|
||||
.selector {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pager button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.left svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.right svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10px;
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
import React from 'react';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import DropDown from './DropDown';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { endOfYear } from 'date-fns';
|
||||
import Modal from './Modal';
|
||||
import DropDown from './DropDown';
|
||||
import DatePickerForm from 'components/forms/DatePickerForm';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
|
||||
const filterOptions = [
|
||||
{
|
||||
@ -35,14 +40,53 @@ const filterOptions = [
|
||||
value: '1month',
|
||||
},
|
||||
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
|
||||
{
|
||||
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
|
||||
value: 'custom',
|
||||
},
|
||||
];
|
||||
|
||||
export default function DateFilter({ value, onChange, className }) {
|
||||
export default function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
const [locale] = useLocale();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const displayValue =
|
||||
value === 'custom'
|
||||
? `${dateFormat(startDate, 'd LLL y', locale)} — ${dateFormat(endDate, 'd LLL y', locale)}`
|
||||
: value;
|
||||
|
||||
function handleChange(value) {
|
||||
if (value === 'custom') {
|
||||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
onChange(getDateRange(value));
|
||||
}
|
||||
|
||||
function handlePickerChange(value) {
|
||||
setShowPicker(false);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropDown className={className} value={value} options={filterOptions} onChange={handleChange} />
|
||||
<>
|
||||
<DropDown
|
||||
className={className}
|
||||
value={displayValue}
|
||||
options={filterOptions}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{showPicker && (
|
||||
<Modal>
|
||||
<DatePickerForm
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
minDate={new Date(2000, 0, 1)}
|
||||
maxDate={endOfYear(new Date())}
|
||||
onChange={handlePickerChange}
|
||||
onClose={() => setShowPicker(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -23,9 +23,8 @@ export default function DropDown({
|
||||
function handleSelect(selected, e) {
|
||||
e.stopPropagation();
|
||||
setShowMenu(false);
|
||||
if (selected !== value) {
|
||||
onChange(selected);
|
||||
}
|
||||
|
||||
onChange(selected);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
@ -37,7 +36,7 @@ export default function DropDown({
|
||||
return (
|
||||
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
||||
<div className={styles.value}>
|
||||
{options.find(e => e.value === value)?.label}
|
||||
<div>{options.find(e => e.value === value)?.label || value}</div>
|
||||
<Icon icon={<Chevron />} size="small" />
|
||||
</div>
|
||||
{showMenu && (
|
||||
|
@ -1,16 +1,15 @@
|
||||
.dropdown {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
font-size: var(--font-size-small);
|
||||
min-width: 140px;
|
||||
border: 1px solid var(--gray500);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
font-size: var(--font-size-small);
|
||||
min-width: 140px;
|
||||
padding: 4px 16px;
|
||||
border: 1px solid var(--gray500);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Icon.module.css';
|
||||
|
||||
export default function Icon({ icon, className, size = 'medium' }) {
|
||||
export default function Icon({ icon, className, size = 'medium', ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.icon, className, {
|
||||
@ -12,6 +12,7 @@ export default function Icon({ icon, className, size = 'medium' }) {
|
||||
[styles.small]: size === 'small',
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
|
@ -35,7 +35,7 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l
|
||||
rel="stylesheet"
|
||||
/>
|
||||
)}
|
||||
{locale === 'jp-JP' && (
|
||||
{locale === 'ja-JP' && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
|
@ -1,5 +1,5 @@
|
||||
.modal {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@ -28,6 +28,7 @@
|
||||
background: var(--gray50);
|
||||
min-width: 400px;
|
||||
min-height: 100px;
|
||||
max-width: 100vw;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--gray300);
|
||||
padding: 30px;
|
||||
|
41
components/forms/DatePickerForm.js
Normal file
41
components/forms/DatePickerForm.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { isAfter } from 'date-fns';
|
||||
import Calendar from 'components/common/Calendar';
|
||||
import Button from 'components/common/Button';
|
||||
import { FormButtons } from 'components/layout/FormLayout';
|
||||
import { getDateRangeValues } from 'lib/date';
|
||||
import styles from './DatePickerForm.module.css';
|
||||
|
||||
export default function DatePickerForm({
|
||||
startDate: defaultStartDate,
|
||||
endDate: defaultEndDate,
|
||||
minDate,
|
||||
maxDate,
|
||||
onChange,
|
||||
onClose,
|
||||
}) {
|
||||
const [startDate, setStartDate] = useState(defaultStartDate);
|
||||
const [endDate, setEndDate] = useState(defaultEndDate);
|
||||
|
||||
function handleSave() {
|
||||
onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.calendars}>
|
||||
<Calendar date={startDate} minDate={minDate} maxDate={endDate} onChange={setStartDate} />
|
||||
<Calendar date={endDate} minDate={startDate} maxDate={maxDate} onChange={setEndDate} />
|
||||
</div>
|
||||
<FormButtons>
|
||||
<Button variant="action" onClick={handleSave} disabled={isAfter(startDate, endDate)}>
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</div>
|
||||
);
|
||||
}
|
25
components/forms/DatePickerForm.module.css
Normal file
25
components/forms/DatePickerForm.module.css
Normal file
@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 800px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.calendars {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.calendars > div:first-child {
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid var(--gray300);
|
||||
}
|
||||
|
||||
.calendars > div:last-child {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.calendars {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
@ -10,10 +10,12 @@ import FormLayout, {
|
||||
} from 'components/layout/FormLayout';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const CONFIRMATION_WORD = 'DELETE';
|
||||
|
||||
const validate = ({ confirmation }) => {
|
||||
const errors = {};
|
||||
|
||||
if (confirmation !== 'DELETE') {
|
||||
if (confirmation !== CONFIRMATION_WORD) {
|
||||
errors.confirmation = !confirmation ? (
|
||||
<FormattedMessage id="label.required" defaultMessage="Required" />
|
||||
) : (
|
||||
@ -44,7 +46,7 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
{props => (
|
||||
<Form>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
@ -63,7 +65,7 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
||||
<FormattedMessage
|
||||
id="message.type-delete"
|
||||
defaultMessage="Type {delete} in the box below to confirm."
|
||||
values={{ delete: <b>DELETE</b> }}
|
||||
values={{ delete: <b>{CONFIRMATION_WORD}</b> }}
|
||||
/>
|
||||
</p>
|
||||
<FormRow>
|
||||
@ -71,7 +73,11 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
||||
<FormError name="confirmation" />
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="danger">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="danger"
|
||||
disabled={props.values.confirmation !== CONFIRMATION_WORD}
|
||||
>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
|
@ -3,7 +3,6 @@ import { useDispatch } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import PageviewsChart from './PageviewsChart';
|
||||
import MetricsBar from './MetricsBar';
|
||||
import QuickButtons from './QuickButtons';
|
||||
import DateFilter from 'components/common/DateFilter';
|
||||
import StickyHeader from 'components/helpers/StickyHeader';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
@ -58,10 +57,12 @@ export default function WebsiteChart({
|
||||
stickyClassName={styles.sticky}
|
||||
enabled={stickyHeader}
|
||||
>
|
||||
<MetricsBar className="col-12 col-md-9 col-lg-10" websiteId={websiteId} />
|
||||
<MetricsBar className="col-12 col-md-9" websiteId={websiteId} />
|
||||
<DateFilter
|
||||
className="col-12 col-md-3 col-lg-2"
|
||||
className="col-12 col-md-3"
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
</StickyHeader>
|
||||
@ -74,7 +75,6 @@ export default function WebsiteChart({
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
/>
|
||||
<QuickButtons value={value} onChange={handleDateChange} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
11
lib/array.js
Normal file
11
lib/array.js
Normal file
@ -0,0 +1,11 @@
|
||||
export function chunk(arr, size) {
|
||||
const chunks = [];
|
||||
|
||||
let index = 0;
|
||||
while (index < arr.length) {
|
||||
chunks.push(arr.slice(index, size + index));
|
||||
index += size;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
15
lib/date.js
15
lib/date.js
@ -18,7 +18,7 @@ import {
|
||||
endOfYear,
|
||||
differenceInHours,
|
||||
differenceInCalendarDays,
|
||||
differenceInMonths,
|
||||
differenceInCalendarMonths,
|
||||
} from 'date-fns';
|
||||
|
||||
export function getTimezone() {
|
||||
@ -85,10 +85,21 @@ export function getDateRange(value) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getDateRangeValues(startDate, endDate) {
|
||||
if (differenceInHours(endDate, startDate) <= 48) {
|
||||
return { startDate: startOfHour(startDate), endDate: endOfHour(endDate), unit: 'hour' };
|
||||
} else if (differenceInCalendarDays(endDate, startDate) <= 90) {
|
||||
return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit: 'day' };
|
||||
} else if (differenceInCalendarMonths(endDate, startDate) <= 12) {
|
||||
return { startDate: startOfMonth(startDate), endDate: endOfMonth(endDate), unit: 'month' };
|
||||
}
|
||||
return { startDate: startOfYear(startDate), endDate: endOfYear(endDate), unit: 'year' };
|
||||
}
|
||||
|
||||
const dateFuncs = {
|
||||
hour: [differenceInHours, addHours, startOfHour],
|
||||
day: [differenceInCalendarDays, addDays, startOfDay],
|
||||
month: [differenceInMonths, addMonths, startOfMonth],
|
||||
month: [differenceInCalendarMonths, addMonths, startOfMonth],
|
||||
};
|
||||
|
||||
export function getDateArray(data, startDate, endDate, unit) {
|
||||
|
@ -9,7 +9,7 @@ import deDEMessages from 'lang-compiled/de-DE.json';
|
||||
import jaMessages from 'lang-compiled/ja-JP.json';
|
||||
|
||||
export const messages = {
|
||||
en: enMessages,
|
||||
'en-US': enMessages,
|
||||
'nl-NL': nlMessages,
|
||||
'zh-CN': zhCNMessages,
|
||||
'de-DE': deDEMessages,
|
||||
@ -19,7 +19,7 @@ export const messages = {
|
||||
};
|
||||
|
||||
export const dateLocales = {
|
||||
en: enUS,
|
||||
'en-US': enUS,
|
||||
'nl-NL': nl,
|
||||
'zh-CN': zhCN,
|
||||
'de-DE': de,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "0.29.0",
|
||||
"version": "0.30.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
|
@ -15,7 +15,10 @@ body {
|
||||
|
||||
.zh-CN {
|
||||
font-family: 'Noto Sans SC', sans-serif !important;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.ja-JP {
|
||||
font-family: 'Noto Sans JP', sans-serif !important;
|
||||
}
|
||||
|
||||
*,
|
||||
@ -40,6 +43,10 @@ h6 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#__modals {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
|
Loading…
Reference in New Issue
Block a user