mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-19 15:53:39 +01:00
commit
d6877af74c
@ -92,7 +92,6 @@ Or with MySQL support:
|
|||||||
docker pull ghcr.io/mikecao/umami:mysql-latest
|
docker pull ghcr.io/mikecao/umami:mysql-latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Getting updates
|
## Getting updates
|
||||||
|
|
||||||
To get the latest features, simply do a pull, install any new dependencies, and rebuild:
|
To get the latest features, simply do a pull, install any new dependencies, and rebuild:
|
||||||
|
1
assets/calendar-alt.svg
Normal file
1
assets/calendar-alt.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M400 64h-48V12c0-6.6-5.4-12-12-12h-8c-6.6 0-12 5.4-12 12v52H128V12c0-6.6-5.4-12-12-12h-8c-6.6 0-12 5.4-12 12v52H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM48 96h352c8.8 0 16 7.2 16 16v48H32v-48c0-8.8 7.2-16 16-16zm352 384H48c-8.8 0-16-7.2-16-16V192h384v272c0 8.8-7.2 16-16 16zM148 320h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm96 0h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm96 0h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm-96 96h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm-96 0h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm192 0h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12z"/></svg>
|
After Width: | Height: | Size: 1002 B |
1
assets/list-ul.svg
Normal file
1
assets/list-ul.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M48 368a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0-160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0-160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm448 24H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V88a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16z"/></svg>
|
After Width: | Height: | Size: 492 B |
@ -28,6 +28,7 @@ export default function WebsiteDetails({ websiteId }) {
|
|||||||
|
|
||||||
const BackButton = () => (
|
const BackButton = () => (
|
||||||
<Button
|
<Button
|
||||||
|
key="back-button"
|
||||||
className={styles.backButton}
|
className={styles.backButton}
|
||||||
icon={<Arrow />}
|
icon={<Arrow />}
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
|
@ -9,9 +9,9 @@ import useFetch from 'hooks/useFetch';
|
|||||||
import Arrow from 'assets/arrow-right.svg';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
import styles from './WebsiteList.module.css';
|
import styles from './WebsiteList.module.css';
|
||||||
|
|
||||||
export default function WebsiteList() {
|
export default function WebsiteList({ userId }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = useFetch('/api/websites');
|
const { data } = useFetch('/api/websites', { userId });
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -13,6 +13,8 @@ export default function Button({
|
|||||||
className,
|
className,
|
||||||
tooltip,
|
tooltip,
|
||||||
tooltipId,
|
tooltipId,
|
||||||
|
disabled = false,
|
||||||
|
onClick = () => {},
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -27,7 +29,11 @@ export default function Button({
|
|||||||
[styles.xsmall]: size === 'xsmall',
|
[styles.xsmall]: size === 'xsmall',
|
||||||
[styles.action]: variant === 'action',
|
[styles.action]: variant === 'action',
|
||||||
[styles.danger]: variant === 'danger',
|
[styles.danger]: variant === 'danger',
|
||||||
|
[styles.light]: variant === 'light',
|
||||||
|
[styles.disabled]: disabled,
|
||||||
})}
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={!disabled ? onClick : null}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{icon && <Icon icon={icon} size={size} />}
|
{icon && <Icon icon={icon} size={size} />}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background: #eaeaea;
|
background: var(--gray200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:active {
|
.button:active {
|
||||||
@ -38,19 +38,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
color: var(--gray50) !important;
|
color: var(--gray50);
|
||||||
background: var(--gray900) !important;
|
background: var(--gray900);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action:hover {
|
.action:hover {
|
||||||
background: var(--gray800) !important;
|
background: var(--gray800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger {
|
.danger {
|
||||||
color: var(--gray50) !important;
|
color: var(--gray50);
|
||||||
background: var(--red500) !important;
|
background: var(--red500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger:hover {
|
.danger:hover {
|
||||||
background: var(--red400) !important;
|
background: var(--red400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
background: var(--gray50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light:hover {
|
||||||
|
background: var(--gray75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
color: var(--gray500);
|
||||||
|
background: var(--gray75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled:active {
|
||||||
|
color: var(--gray500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled:hover {
|
||||||
|
background: var(--gray75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.light:disabled {
|
||||||
|
background: var(--gray50);
|
||||||
}
|
}
|
||||||
|
261
components/common/Calendar.js
Normal file
261
components/common/Calendar.js
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
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(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(endOfMonth(month), minDate) || isAfter(startOfMonth(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="small"
|
||||||
|
className={styles.left}
|
||||||
|
onClick={handlePrevClick}
|
||||||
|
disabled={years[0] <= minYear}
|
||||||
|
variant="light"
|
||||||
|
/>
|
||||||
|
<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="small"
|
||||||
|
className={styles.right}
|
||||||
|
onClick={handleNextClick}
|
||||||
|
disabled={years[years.length - 1] > maxYear}
|
||||||
|
variant="light"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
85
components/common/Calendar.module.css
Normal file
85
components/common/Calendar.module.css
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
.calendar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
flex: 1;
|
||||||
|
min-height: 285px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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(--gray400);
|
||||||
|
background: var(--gray75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td.disabled:hover {
|
||||||
|
cursor: default;
|
||||||
|
background: var(--gray75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar td.faded.disabled {
|
||||||
|
background: var(--gray100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,21 +1,39 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { getDateRange } from 'lib/date';
|
|
||||||
import DropDown from './DropDown';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
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';
|
||||||
|
import Calendar from 'assets/calendar-alt.svg';
|
||||||
|
import Icon from './Icon';
|
||||||
|
|
||||||
const filterOptions = [
|
const filterOptions = [
|
||||||
|
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} />
|
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} />
|
||||||
),
|
),
|
||||||
value: '24hour',
|
value: '24hour',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
|
||||||
|
value: '1week',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
|
||||||
),
|
),
|
||||||
value: '7day',
|
value: '7day',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
|
||||||
|
value: '1month',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
|
||||||
@ -28,21 +46,73 @@ const filterOptions = [
|
|||||||
),
|
),
|
||||||
value: '90day',
|
value: '90day',
|
||||||
},
|
},
|
||||||
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
|
|
||||||
{ label: <FormattedMessage id="label.this-week" defaultMessage="This week" />, value: '1week' },
|
|
||||||
{
|
|
||||||
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
|
|
||||||
value: '1month',
|
|
||||||
},
|
|
||||||
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
|
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
|
||||||
|
value: 'custom',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function DateFilter({ value, onChange, className }) {
|
export default function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||||
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
|
const displayValue =
|
||||||
|
value === 'custom' ? (
|
||||||
|
<CustomRange startDate={startDate} endDate={endDate} onClick={() => handleChange('custom')} />
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
);
|
||||||
|
|
||||||
function handleChange(value) {
|
function handleChange(value) {
|
||||||
|
if (value === 'custom') {
|
||||||
|
setShowPicker(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
onChange(getDateRange(value));
|
onChange(getDateRange(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePickerChange(value) {
|
||||||
|
setShowPicker(false);
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CustomRange = ({ startDate, endDate, onClick }) => {
|
||||||
|
const [locale] = useLocale();
|
||||||
|
|
||||||
|
function handleClick(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Icon icon={<Calendar />} className="mr-2" onClick={handleClick} />
|
||||||
|
{`${dateFormat(startDate, 'd LLL y', locale)} — ${dateFormat(endDate, 'd LLL y', locale)}`}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -23,9 +23,8 @@ export default function DropDown({
|
|||||||
function handleSelect(selected, e) {
|
function handleSelect(selected, e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
if (selected !== value) {
|
|
||||||
onChange(selected);
|
onChange(selected);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useDocumentClick(e => {
|
useDocumentClick(e => {
|
||||||
@ -37,8 +36,8 @@ export default function DropDown({
|
|||||||
return (
|
return (
|
||||||
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
||||||
<div className={styles.value}>
|
<div className={styles.value}>
|
||||||
{options.find(e => e.value === value)?.label}
|
{options.find(e => e.value === value)?.label || value}
|
||||||
<Icon icon={<Chevron />} size="small" />
|
<Icon icon={<Chevron />} className={styles.icon} size="small" />
|
||||||
</div>
|
</div>
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<Menu className={menuClassName} options={options} onSelect={handleSelect} float="bottom" />
|
<Menu className={menuClassName} options={options} onSelect={handleSelect} float="bottom" />
|
||||||
|
@ -1,16 +1,24 @@
|
|||||||
.dropdown {
|
.dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: var(--font-size-small);
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
white-space: nowrap;
|
align-items: center;
|
||||||
position: relative;
|
|
||||||
padding: 4px 16px;
|
|
||||||
border: 1px solid var(--gray500);
|
border: 1px solid var(--gray500);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 4px 16px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Icon.module.css';
|
import styles from './Icon.module.css';
|
||||||
|
|
||||||
export default function Icon({ icon, className, size = 'medium' }) {
|
export default function Icon({ icon, className, size = 'medium', ...props }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.icon, className, {
|
className={classNames(styles.icon, className, {
|
||||||
@ -12,6 +12,7 @@ export default function Icon({ icon, className, size = 'medium' }) {
|
|||||||
[styles.small]: size === 'small',
|
[styles.small]: size === 'small',
|
||||||
[styles.xsmall]: size === 'xsmall',
|
[styles.xsmall]: size === 'xsmall',
|
||||||
})}
|
})}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
|
import Head from 'next/head';
|
||||||
import Globe from 'assets/globe.svg';
|
import Globe from 'assets/globe.svg';
|
||||||
import useDocumentClick from 'hooks/useDocumentClick';
|
import useDocumentClick from 'hooks/useDocumentClick';
|
||||||
import Menu from './Menu';
|
import Menu from './Menu';
|
||||||
@ -19,6 +20,10 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l
|
|||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
setShowMenu(state => !state);
|
||||||
|
}
|
||||||
|
|
||||||
useDocumentClick(e => {
|
useDocumentClick(e => {
|
||||||
if (!ref.current.contains(e.target)) {
|
if (!ref.current.contains(e.target)) {
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
@ -26,19 +31,35 @@ export default function LanguageButton({ menuPosition = 'bottom', menuAlign = 'l
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={styles.container}>
|
<>
|
||||||
<Button icon={<Globe />} onClick={() => setShowMenu(true)} size="small">
|
<Head>
|
||||||
<div className={locale}>{selectedLocale}</div>
|
{locale === 'zh-CN' && (
|
||||||
</Button>
|
<link
|
||||||
{showMenu && (
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
||||||
<Menu
|
rel="stylesheet"
|
||||||
className={styles.menu}
|
/>
|
||||||
options={menuOptions}
|
)}
|
||||||
onSelect={handleSelect}
|
{locale === 'ja-JP' && (
|
||||||
float={menuPosition}
|
<link
|
||||||
align={menuAlign}
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap"
|
||||||
/>
|
rel="stylesheet"
|
||||||
)}
|
/>
|
||||||
</div>
|
)}
|
||||||
|
</Head>
|
||||||
|
<div ref={ref} className={styles.container}>
|
||||||
|
<Button icon={<Globe />} onClick={toggleMenu} size="small">
|
||||||
|
<div className={locale}>{selectedLocale}</div>
|
||||||
|
</Button>
|
||||||
|
{showMenu && (
|
||||||
|
<Menu
|
||||||
|
className={styles.menu}
|
||||||
|
options={menuOptions}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
float={menuPosition}
|
||||||
|
align={menuAlign}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ export default function Menu({
|
|||||||
{options
|
{options
|
||||||
.filter(({ hidden }) => !hidden)
|
.filter(({ hidden }) => !hidden)
|
||||||
.map(option => {
|
.map(option => {
|
||||||
const { label, value, className: customClassName, render } = option;
|
const { label, value, className: customClassName, render, divider } = option;
|
||||||
|
|
||||||
return render ? (
|
return render ? (
|
||||||
render(option)
|
render(option)
|
||||||
@ -34,6 +34,7 @@ export default function Menu({
|
|||||||
key={value}
|
key={value}
|
||||||
className={classNames(styles.option, optionClassName, customClassName, {
|
className={classNames(styles.option, optionClassName, customClassName, {
|
||||||
[selectedClassName]: selectedOption === value,
|
[selectedClassName]: selectedOption === value,
|
||||||
|
[styles.divider]: divider,
|
||||||
})}
|
})}
|
||||||
onClick={e => onSelect(value, e)}
|
onClick={e => onSelect(value, e)}
|
||||||
>
|
>
|
||||||
|
@ -40,3 +40,7 @@
|
|||||||
.right {
|
.right {
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border-top: 1px solid var(--gray300);
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.modal {
|
.modal {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -28,6 +28,7 @@
|
|||||||
background: var(--gray50);
|
background: var(--gray50);
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
|
max-width: 100vw;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border: 1px solid var(--gray300);
|
border: 1px solid var(--gray300);
|
||||||
padding: 30px;
|
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';
|
} from 'components/layout/FormLayout';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
const CONFIRMATION_WORD = 'DELETE';
|
||||||
|
|
||||||
const validate = ({ confirmation }) => {
|
const validate = ({ confirmation }) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
|
||||||
if (confirmation !== 'DELETE') {
|
if (confirmation !== CONFIRMATION_WORD) {
|
||||||
errors.confirmation = !confirmation ? (
|
errors.confirmation = !confirmation ? (
|
||||||
<FormattedMessage id="label.required" defaultMessage="Required" />
|
<FormattedMessage id="label.required" defaultMessage="Required" />
|
||||||
) : (
|
) : (
|
||||||
@ -44,7 +46,7 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
|||||||
validate={validate}
|
validate={validate}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{() => (
|
{props => (
|
||||||
<Form>
|
<Form>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -63,7 +65,7 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="message.type-delete"
|
id="message.type-delete"
|
||||||
defaultMessage="Type {delete} in the box below to confirm."
|
defaultMessage="Type {delete} in the box below to confirm."
|
||||||
values={{ delete: <b>DELETE</b> }}
|
values={{ delete: <b>{CONFIRMATION_WORD}</b> }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
@ -71,7 +73,11 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
|||||||
<FormError name="confirmation" />
|
<FormError name="confirmation" />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<Button type="submit" variant="danger">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="danger"
|
||||||
|
disabled={props.values.confirmation !== CONFIRMATION_WORD}
|
||||||
|
>
|
||||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose}>
|
<Button onClick={onClose}>
|
||||||
|
@ -13,10 +13,6 @@ export default function Layout({ title, children, header = true, footer = true }
|
|||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
{header && <Header />}
|
{header && <Header />}
|
||||||
<main className="container">{children}</main>
|
<main className="container">{children}</main>
|
||||||
|
@ -44,9 +44,9 @@ export default function BarChart({
|
|||||||
return dateFormat(d, 'EEE M/d', locale);
|
return dateFormat(d, 'EEE M/d', locale);
|
||||||
case 'month':
|
case 'month':
|
||||||
if (w <= 660) {
|
if (w <= 660) {
|
||||||
return dateFormat(d, 'MMM', locale);
|
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
|
||||||
}
|
}
|
||||||
return dateFormat(d, 'MMMM', locale);
|
return dateFormat(d, 'MMM', locale);
|
||||||
default:
|
default:
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
@ -64,14 +64,8 @@ export default function BarChart({
|
|||||||
} else {
|
} else {
|
||||||
const [label, value] = body[0].lines[0].split(':');
|
const [label, value] = body[0].lines[0].split(':');
|
||||||
|
|
||||||
console.log(
|
|
||||||
+title[0],
|
|
||||||
new Date(+title[0]),
|
|
||||||
dateFormat(new Date(+title[0]), 'EEE MMMM d yyyy', locale),
|
|
||||||
);
|
|
||||||
|
|
||||||
setTooltip({
|
setTooltip({
|
||||||
title: dateFormat(new Date(+title[0]), 'EEE MMMM d yyyy', locale),
|
title: dateFormat(new Date(+title[0]), getTooltipFormat(unit), locale),
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
labelColor: labelColors[0].backgroundColor,
|
labelColor: labelColors[0].backgroundColor,
|
||||||
@ -79,6 +73,15 @@ export default function BarChart({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTooltipFormat(unit) {
|
||||||
|
switch (unit) {
|
||||||
|
case 'hour':
|
||||||
|
return 'EEE ha — MMM d yyyy';
|
||||||
|
default:
|
||||||
|
return 'EEE MMMM d yyyy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createChart() {
|
function createChart() {
|
||||||
const options = {
|
const options = {
|
||||||
animation: {
|
animation: {
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 120px;
|
min-width: 140px;
|
||||||
margin-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
@ -16,4 +16,5 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: var(--font-size-normal);
|
font-size: var(--font-size-normal);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 992px) {
|
@media only screen and (max-width: 992px) {
|
||||||
|
.bar {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
.bar > div:last-child {
|
.bar > div:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { useDispatch } from 'react-redux';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PageviewsChart from './PageviewsChart';
|
import PageviewsChart from './PageviewsChart';
|
||||||
import MetricsBar from './MetricsBar';
|
import MetricsBar from './MetricsBar';
|
||||||
import QuickButtons from './QuickButtons';
|
|
||||||
import DateFilter from 'components/common/DateFilter';
|
import DateFilter from 'components/common/DateFilter';
|
||||||
import StickyHeader from 'components/helpers/StickyHeader';
|
import StickyHeader from 'components/helpers/StickyHeader';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
@ -58,12 +57,17 @@ export default function WebsiteChart({
|
|||||||
stickyClassName={styles.sticky}
|
stickyClassName={styles.sticky}
|
||||||
enabled={stickyHeader}
|
enabled={stickyHeader}
|
||||||
>
|
>
|
||||||
<MetricsBar className="col-12 col-md-9 col-lg-10" websiteId={websiteId} />
|
<div className="col-12 col-lg-9">
|
||||||
<DateFilter
|
<MetricsBar websiteId={websiteId} />
|
||||||
className="col-12 col-md-3 col-lg-2"
|
</div>
|
||||||
value={value}
|
<div className={classNames(styles.filter, 'col-12 col-lg-3')}>
|
||||||
onChange={handleDateChange}
|
<DateFilter
|
||||||
/>
|
value={value}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</StickyHeader>
|
</StickyHeader>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@ -74,7 +78,6 @@ export default function WebsiteChart({
|
|||||||
unit={unit}
|
unit={unit}
|
||||||
records={getDateLength(startDate, endDate, unit)}
|
records={getDateLength(startDate, endDate, unit)}
|
||||||
/>
|
/>
|
||||||
<QuickButtons value={value} onChange={handleDateChange} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -29,3 +29,15 @@
|
|||||||
border-bottom: 1px solid var(--gray300);
|
border-bottom: 1px solid var(--gray300);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 992px) {
|
||||||
|
.filter {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import Table from 'components/common/Table';
|
import Table from 'components/common/Table';
|
||||||
import Modal from 'components/common/Modal';
|
import Modal from 'components/common/Modal';
|
||||||
|
import Toast from 'components/common/Toast';
|
||||||
import AccountEditForm from 'components/forms/AccountEditForm';
|
import AccountEditForm from 'components/forms/AccountEditForm';
|
||||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import DeleteForm from 'components/forms/DeleteForm';
|
import DeleteForm from 'components/forms/DeleteForm';
|
||||||
@ -13,11 +16,11 @@ import Pen from 'assets/pen.svg';
|
|||||||
import Plus from 'assets/plus.svg';
|
import Plus from 'assets/plus.svg';
|
||||||
import Trash from 'assets/trash.svg';
|
import Trash from 'assets/trash.svg';
|
||||||
import Check from 'assets/check.svg';
|
import Check from 'assets/check.svg';
|
||||||
|
import List from 'assets/list-ul.svg';
|
||||||
import styles from './AccountSettings.module.css';
|
import styles from './AccountSettings.module.css';
|
||||||
import Toast from '../common/Toast';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
export default function AccountSettings() {
|
export default function AccountSettings() {
|
||||||
|
const router = useRouter();
|
||||||
const [addAccount, setAddAccount] = useState();
|
const [addAccount, setAddAccount] = useState();
|
||||||
const [editAccount, setEditAccount] = useState();
|
const [editAccount, setEditAccount] = useState();
|
||||||
const [deleteAccount, setDeleteAccount] = useState();
|
const [deleteAccount, setDeleteAccount] = useState();
|
||||||
@ -30,6 +33,13 @@ export default function AccountSettings() {
|
|||||||
const Buttons = row =>
|
const Buttons = row =>
|
||||||
row.username !== 'admin' ? (
|
row.username !== 'admin' ? (
|
||||||
<ButtonLayout>
|
<ButtonLayout>
|
||||||
|
<Button
|
||||||
|
icon={<List />}
|
||||||
|
size="small"
|
||||||
|
tooltip={<FormattedMessage id="button.websites" defaultMessage="Websites" />}
|
||||||
|
tooltipId={`button-websites-${row.username}`}
|
||||||
|
onClick={() => router.push(`/dashboard/${row.user_id}/${row.username}`)}
|
||||||
|
/>
|
||||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||||
@ -56,6 +66,7 @@ export default function AccountSettings() {
|
|||||||
render: Checkmark,
|
render: Checkmark,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'actions',
|
||||||
className: classNames(styles.buttons, 'col-12 col-md-4 pt-2 pt-md-0'),
|
className: classNames(styles.buttons, 'col-12 col-md-4 pt-2 pt-md-0'),
|
||||||
render: Buttons,
|
render: Buttons,
|
||||||
},
|
},
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "Mehr",
|
"button.more": "Mehr",
|
||||||
"button.save": "Speichern",
|
"button.save": "Speichern",
|
||||||
"button.view-details": "Details anzeigen",
|
"button.view-details": "Details anzeigen",
|
||||||
|
"button.websites": "Webseiten",
|
||||||
"footer.powered-by": "Powered by",
|
"footer.powered-by": "Powered by",
|
||||||
"header.nav.dashboard": "Übersicht",
|
"header.nav.dashboard": "Übersicht",
|
||||||
"header.nav.settings": "Einstellungen",
|
"header.nav.settings": "Einstellungen",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.confirm-password": "Passwort wiederholen",
|
"label.confirm-password": "Passwort wiederholen",
|
||||||
"label.current-password": "Derzeitiges Passwort",
|
"label.current-password": "Derzeitiges Passwort",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "Domain",
|
"label.domain": "Domain",
|
||||||
"label.enable-share-url": "Freigabe-URL aktivieren",
|
"label.enable-share-url": "Freigabe-URL aktivieren",
|
||||||
"label.invalid": "Ungültig",
|
"label.invalid": "Ungültig",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "Es it ein Fehler aufgetreten.",
|
"message.failure": "Es it ein Fehler aufgetreten.",
|
||||||
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
|
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
|
||||||
"message.no-data-available": "Keine Daten vorhanden.",
|
"message.no-data-available": "Keine Daten vorhanden.",
|
||||||
|
"message.page-not-found": "Seite nicht gefunden.",
|
||||||
"message.save-success": "Erfolgreich gespeichert.",
|
"message.save-success": "Erfolgreich gespeichert.",
|
||||||
"message.share-url": "Dies ist der öffentliche URL zum Teilen für {target}.",
|
"message.share-url": "Dies ist der öffentliche URL zum Teilen für {target}.",
|
||||||
"message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Homepage.",
|
"message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Homepage.",
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "More",
|
"button.more": "More",
|
||||||
"button.save": "Save",
|
"button.save": "Save",
|
||||||
"button.view-details": "View details",
|
"button.view-details": "View details",
|
||||||
|
"button.websites": "Websites",
|
||||||
"footer.powered-by": "Powered by",
|
"footer.powered-by": "Powered by",
|
||||||
"header.nav.dashboard": "Dashboard",
|
"header.nav.dashboard": "Dashboard",
|
||||||
"header.nav.settings": "Settings",
|
"header.nav.settings": "Settings",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.confirm-password": "Confirm password",
|
"label.confirm-password": "Confirm password",
|
||||||
"label.current-password": "Current password",
|
"label.current-password": "Current password",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "Domain",
|
"label.domain": "Domain",
|
||||||
"label.enable-share-url": "Enable share URL",
|
"label.enable-share-url": "Enable share URL",
|
||||||
"label.invalid": "Invalid",
|
"label.invalid": "Invalid",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "Something went wrong.",
|
"message.failure": "Something went wrong.",
|
||||||
"message.incorrect-username-password": "Incorrect username/password.",
|
"message.incorrect-username-password": "Incorrect username/password.",
|
||||||
"message.no-data-available": "No data available.",
|
"message.no-data-available": "No data available.",
|
||||||
|
"message.page-not-found": "Page not found.",
|
||||||
"message.save-success": "Saved successfully.",
|
"message.save-success": "Saved successfully.",
|
||||||
"message.share-url": "This is the publicly shared URL for {target}.",
|
"message.share-url": "This is the publicly shared URL for {target}.",
|
||||||
"message.track-stats": "To track stats for {target}, place the following code in the {head} section of your website.",
|
"message.track-stats": "To track stats for {target}, place the following code in the {head} section of your website.",
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "Más",
|
"button.more": "Más",
|
||||||
"button.save": "Guardar",
|
"button.save": "Guardar",
|
||||||
"button.view-details": "Ver detalles",
|
"button.view-details": "Ver detalles",
|
||||||
|
"button.websites": "Sitios",
|
||||||
"footer.powered-by": "Desarrollado con",
|
"footer.powered-by": "Desarrollado con",
|
||||||
"header.nav.dashboard": "Panel de control",
|
"header.nav.dashboard": "Panel de control",
|
||||||
"header.nav.settings": "Configuraciones",
|
"header.nav.settings": "Configuraciones",
|
||||||
"label.administrator": "Administrador",
|
"label.administrator": "Administrador",
|
||||||
"label.confirm-password": "Confirmar contraseña",
|
"label.confirm-password": "Confirmar contraseña",
|
||||||
"label.current-password": "Contraseña actual",
|
"label.current-password": "Contraseña actual",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "Dominio",
|
"label.domain": "Dominio",
|
||||||
"label.enable-share-url": "Habilitar compartir URL",
|
"label.enable-share-url": "Habilitar compartir URL",
|
||||||
"label.invalid": "Inválido",
|
"label.invalid": "Inválido",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "Algo falló.",
|
"message.failure": "Algo falló.",
|
||||||
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
|
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
|
||||||
"message.no-data-available": "Sin información disponible.",
|
"message.no-data-available": "Sin información disponible.",
|
||||||
|
"message.page-not-found": "Page not found",
|
||||||
"message.save-success": "Guardado exitosamente.",
|
"message.save-success": "Guardado exitosamente.",
|
||||||
"message.share-url": "Esta es la URL compartida públicamente para {target}.",
|
"message.share-url": "Esta es la URL compartida públicamente para {target}.",
|
||||||
"message.track-stats": "Para registrar estadísticas para {target}, copia el siguiente código dentro de la etiqueta {head} de tu sitio.",
|
"message.track-stats": "Para registrar estadísticas para {target}, copia el siguiente código dentro de la etiqueta {head} de tu sitio.",
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "さらに表示",
|
"button.more": "さらに表示",
|
||||||
"button.save": "保存",
|
"button.save": "保存",
|
||||||
"button.view-details": "詳細表示",
|
"button.view-details": "詳細表示",
|
||||||
|
"button.websites": "Webサイト",
|
||||||
"footer.powered-by": "Powered by",
|
"footer.powered-by": "Powered by",
|
||||||
"header.nav.dashboard": "ダッシュボード",
|
"header.nav.dashboard": "ダッシュボード",
|
||||||
"header.nav.settings": "設定",
|
"header.nav.settings": "設定",
|
||||||
"label.administrator": "管理者",
|
"label.administrator": "管理者",
|
||||||
"label.confirm-password": "パスワード(確認)",
|
"label.confirm-password": "パスワード(確認)",
|
||||||
"label.current-password": "現在のパスワード",
|
"label.current-password": "現在のパスワード",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "ドメイン",
|
"label.domain": "ドメイン",
|
||||||
"label.enable-share-url": "共有リンクを有効にする",
|
"label.enable-share-url": "共有リンクを有効にする",
|
||||||
"label.invalid": "無効",
|
"label.invalid": "無効",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "問題が発生しました。",
|
"message.failure": "問題が発生しました。",
|
||||||
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
|
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
|
||||||
"message.no-data-available": "データがありません。",
|
"message.no-data-available": "データがありません。",
|
||||||
|
"message.page-not-found": "ページが見つかりません。",
|
||||||
"message.save-success": "正常に保存されました。",
|
"message.save-success": "正常に保存されました。",
|
||||||
"message.share-url": "これは {target} の共有リンクです。",
|
"message.share-url": "これは {target} の共有リンクです。",
|
||||||
"message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。",
|
"message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。",
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "Toon meer",
|
"button.more": "Toon meer",
|
||||||
"button.save": "Opslaan",
|
"button.save": "Opslaan",
|
||||||
"button.view-details": "Meer details",
|
"button.view-details": "Meer details",
|
||||||
|
"button.websites": "Websites",
|
||||||
"footer.powered-by": "mogelijk gemaakt door",
|
"footer.powered-by": "mogelijk gemaakt door",
|
||||||
"header.nav.dashboard": "Dashboard",
|
"header.nav.dashboard": "Dashboard",
|
||||||
"header.nav.settings": "Instellingen",
|
"header.nav.settings": "Instellingen",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.confirm-password": "Wachtwoord bevestigen",
|
"label.confirm-password": "Wachtwoord bevestigen",
|
||||||
"label.current-password": "Huidig wachtwoord",
|
"label.current-password": "Huidig wachtwoord",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "Domein",
|
"label.domain": "Domein",
|
||||||
"label.enable-share-url": "Sta delen via openbare URL toe",
|
"label.enable-share-url": "Sta delen via openbare URL toe",
|
||||||
"label.invalid": "Ongeldig",
|
"label.invalid": "Ongeldig",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "Er is iets misgegaan.",
|
"message.failure": "Er is iets misgegaan.",
|
||||||
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
||||||
"message.no-data-available": "Geen gegevens beschikbaar.",
|
"message.no-data-available": "Geen gegevens beschikbaar.",
|
||||||
|
"message.page-not-found": "Pagina niet gevonden.",
|
||||||
"message.save-success": "Opslaan succesvol.",
|
"message.save-success": "Opslaan succesvol.",
|
||||||
"message.share-url": "Met deze URL kan {target} openbaar gedeeld worden.",
|
"message.share-url": "Met deze URL kan {target} openbaar gedeeld worden.",
|
||||||
"message.track-stats": "Om statistieken voor {target} bij te houden, plaats je de volgende code in het {head} gedeelte van je website.",
|
"message.track-stats": "Om statistieken voor {target} bij te houden, plaats je de volgende code in het {head} gedeelte van je website.",
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "Больше",
|
"button.more": "Больше",
|
||||||
"button.save": "Сохранить",
|
"button.save": "Сохранить",
|
||||||
"button.view-details": "Посмотреть детали",
|
"button.view-details": "Посмотреть детали",
|
||||||
|
"button.websites": "Сайты",
|
||||||
"footer.powered-by": "на движке",
|
"footer.powered-by": "на движке",
|
||||||
"header.nav.dashboard": "Информационная панель",
|
"header.nav.dashboard": "Информационная панель",
|
||||||
"header.nav.settings": "Настройки",
|
"header.nav.settings": "Настройки",
|
||||||
"label.administrator": "Администратор",
|
"label.administrator": "Администратор",
|
||||||
"label.confirm-password": "Подтвердить пароль",
|
"label.confirm-password": "Подтвердить пароль",
|
||||||
"label.current-password": "Текущий пароль",
|
"label.current-password": "Текущий пароль",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "Домен",
|
"label.domain": "Домен",
|
||||||
"label.enable-share-url": "Разрешить делиться ссылкой",
|
"label.enable-share-url": "Разрешить делиться ссылкой",
|
||||||
"label.invalid": "Некорректный",
|
"label.invalid": "Некорректный",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "Что-то пошло не так.",
|
"message.failure": "Что-то пошло не так.",
|
||||||
"message.incorrect-username-password": "Неверное имя пользователя/пароль.",
|
"message.incorrect-username-password": "Неверное имя пользователя/пароль.",
|
||||||
"message.no-data-available": "Нет данных.",
|
"message.no-data-available": "Нет данных.",
|
||||||
|
"message.page-not-found": "Страница не найдена.",
|
||||||
"message.save-success": "Успешно сохранено.",
|
"message.save-success": "Успешно сохранено.",
|
||||||
"message.share-url": "Это публичная ссылка для {target}.",
|
"message.share-url": "Это публичная ссылка для {target}.",
|
||||||
"message.track-stats": "Чтобы отслеживать статистику для {target}, поместите следующий код в раздел {head} вашего сайта.",
|
"message.track-stats": "Чтобы отслеживать статистику для {target}, поместите следующий код в раздел {head} вашего сайта.",
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "Detaylı göster",
|
"button.more": "Detaylı göster",
|
||||||
"button.save": "Kaydet",
|
"button.save": "Kaydet",
|
||||||
"button.view-details": "Detayı incele",
|
"button.view-details": "Detayı incele",
|
||||||
|
"button.websites": "Web siteleri",
|
||||||
"footer.powered-by": "Sağlayıcı:",
|
"footer.powered-by": "Sağlayıcı:",
|
||||||
"header.nav.dashboard": "Kontrol Paneli",
|
"header.nav.dashboard": "Kontrol Paneli",
|
||||||
"header.nav.settings": "Ayarlar",
|
"header.nav.settings": "Ayarlar",
|
||||||
"label.administrator": "Yönetici",
|
"label.administrator": "Yönetici",
|
||||||
"label.confirm-password": "Parolayı onayla",
|
"label.confirm-password": "Parolayı onayla",
|
||||||
"label.current-password": "Mevcut parola",
|
"label.current-password": "Mevcut parola",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "Alan adı",
|
"label.domain": "Alan adı",
|
||||||
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
|
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
|
||||||
"label.invalid": "Geçeriz",
|
"label.invalid": "Geçeriz",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "Bir şeyler ters gitti!",
|
"message.failure": "Bir şeyler ters gitti!",
|
||||||
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
|
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
|
||||||
"message.no-data-available": "Henüz hiç veri yok.",
|
"message.no-data-available": "Henüz hiç veri yok.",
|
||||||
|
"message.page-not-found": "Sayfa bulunamadı.",
|
||||||
"message.save-success": "Başarıyla kaydedildi.",
|
"message.save-success": "Başarıyla kaydedildi.",
|
||||||
"message.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.",
|
"message.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.",
|
||||||
"message.track-stats": "{target} alanı adı istatistiklerini takip etmek için, aşağıdaki kodu web sitenizin {head} bloğuna yerleştirin.",
|
"message.track-stats": "{target} alanı adı istatistiklerini takip etmek için, aşağıdaki kodu web sitenizin {head} bloğuna yerleştirin.",
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"button.more": "更多",
|
"button.more": "更多",
|
||||||
"button.save": "保存",
|
"button.save": "保存",
|
||||||
"button.view-details": "查看更多",
|
"button.view-details": "查看更多",
|
||||||
|
"button.websites": "网站",
|
||||||
"footer.powered-by": "运行",
|
"footer.powered-by": "运行",
|
||||||
"header.nav.dashboard": "仪表板",
|
"header.nav.dashboard": "仪表板",
|
||||||
"header.nav.settings": "设置",
|
"header.nav.settings": "设置",
|
||||||
"label.administrator": "管理员",
|
"label.administrator": "管理员",
|
||||||
"label.confirm-password": "确认密码",
|
"label.confirm-password": "确认密码",
|
||||||
"label.current-password": "目前密码",
|
"label.current-password": "目前密码",
|
||||||
|
"label.custom-range": "Custom range",
|
||||||
"label.domain": "域名",
|
"label.domain": "域名",
|
||||||
"label.enable-share-url": "激活共享链接",
|
"label.enable-share-url": "激活共享链接",
|
||||||
"label.invalid": "输入无效",
|
"label.invalid": "输入无效",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"message.failure": "出现错误.",
|
"message.failure": "出现错误.",
|
||||||
"message.incorrect-username-password": "用户名密码不正确.",
|
"message.incorrect-username-password": "用户名密码不正确.",
|
||||||
"message.no-data-available": "无可用数据.",
|
"message.no-data-available": "无可用数据.",
|
||||||
|
"message.page-not-found": "网页未找到.",
|
||||||
"message.save-success": "成功保存.",
|
"message.save-success": "成功保存.",
|
||||||
"message.share-url": "这是 {target} 的共享链接.",
|
"message.share-url": "这是 {target} 的共享链接.",
|
||||||
"message.track-stats": "把以下代码放到你的网站的{head}部分来收集{target}的数据.",
|
"message.track-stats": "把以下代码放到你的网站的{head}部分来收集{target}的数据.",
|
||||||
|
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;
|
||||||
|
}
|
29
lib/date.js
29
lib/date.js
@ -4,6 +4,7 @@ import {
|
|||||||
addHours,
|
addHours,
|
||||||
addDays,
|
addDays,
|
||||||
addMonths,
|
addMonths,
|
||||||
|
addYears,
|
||||||
subHours,
|
subHours,
|
||||||
subDays,
|
subDays,
|
||||||
startOfHour,
|
startOfHour,
|
||||||
@ -18,7 +19,8 @@ import {
|
|||||||
endOfYear,
|
endOfYear,
|
||||||
differenceInHours,
|
differenceInHours,
|
||||||
differenceInCalendarDays,
|
differenceInCalendarDays,
|
||||||
differenceInMonths,
|
differenceInCalendarMonths,
|
||||||
|
differenceInCalendarYears,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
|
|
||||||
export function getTimezone() {
|
export function getTimezone() {
|
||||||
@ -85,10 +87,24 @@ export function getDateRange(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDateRangeValues(startDate, endDate) {
|
||||||
|
let unit = 'year';
|
||||||
|
if (differenceInHours(endDate, startDate) <= 72) {
|
||||||
|
unit = 'hour';
|
||||||
|
} else if (differenceInCalendarDays(endDate, startDate) <= 90) {
|
||||||
|
unit = 'day';
|
||||||
|
} else if (differenceInCalendarMonths(endDate, startDate) <= 24) {
|
||||||
|
unit = 'month';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit };
|
||||||
|
}
|
||||||
|
|
||||||
const dateFuncs = {
|
const dateFuncs = {
|
||||||
hour: [differenceInHours, addHours, startOfHour],
|
hour: [differenceInHours, addHours, startOfHour],
|
||||||
day: [differenceInCalendarDays, addDays, startOfDay],
|
day: [differenceInCalendarDays, addDays, startOfDay],
|
||||||
month: [differenceInMonths, addMonths, startOfMonth],
|
month: [differenceInCalendarMonths, addMonths, startOfMonth],
|
||||||
|
year: [differenceInCalendarYears, addYears, startOfYear],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDateArray(data, startDate, endDate, unit) {
|
export function getDateArray(data, startDate, endDate, unit) {
|
||||||
@ -98,11 +114,12 @@ export function getDateArray(data, startDate, endDate, unit) {
|
|||||||
|
|
||||||
function findData(t) {
|
function findData(t) {
|
||||||
const x = data.find(e => {
|
const x = data.find(e => {
|
||||||
if (unit === 'day') {
|
if (unit === 'hour') {
|
||||||
const [year, month, day] = e.t.split('-');
|
return normalize(new Date(e.t)).getTime() === t.getTime();
|
||||||
return normalize(new Date(year, month - 1, day)).getTime() === t.getTime();
|
|
||||||
}
|
}
|
||||||
return normalize(new Date(e.t)).getTime() === t.getTime();
|
|
||||||
|
const [year, month, day] = e.t.split('-');
|
||||||
|
return normalize(new Date(year, month - 1, day)).getTime() === t.getTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
return x?.y || 0;
|
return x?.y || 0;
|
||||||
|
16
lib/lang.js
16
lib/lang.js
@ -10,7 +10,7 @@ import jaMessages from 'lang-compiled/ja-JP.json';
|
|||||||
import esMXMessages from 'lang-compiled/es-MX.json';
|
import esMXMessages from 'lang-compiled/es-MX.json';
|
||||||
|
|
||||||
export const messages = {
|
export const messages = {
|
||||||
en: enMessages,
|
'en-US': enMessages,
|
||||||
'nl-NL': nlMessages,
|
'nl-NL': nlMessages,
|
||||||
'zh-CN': zhCNMessages,
|
'zh-CN': zhCNMessages,
|
||||||
'de-DE': deDEMessages,
|
'de-DE': deDEMessages,
|
||||||
@ -21,7 +21,7 @@ export const messages = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const dateLocales = {
|
export const dateLocales = {
|
||||||
en: enUS,
|
'en-US': enUS,
|
||||||
'nl-NL': nl,
|
'nl-NL': nl,
|
||||||
'zh-CN': zhCN,
|
'zh-CN': zhCN,
|
||||||
'de-DE': de,
|
'de-DE': de,
|
||||||
@ -33,13 +33,13 @@ export const dateLocales = {
|
|||||||
|
|
||||||
export const menuOptions = [
|
export const menuOptions = [
|
||||||
{ label: 'English', value: 'en', display: 'EN' },
|
{ label: 'English', value: 'en', display: 'EN' },
|
||||||
{ label: '中文 (Chinese Simplified)', value: 'zh-CN', display: '中文' },
|
{ label: '中文', value: 'zh-CN', display: 'CN' },
|
||||||
{ label: 'Nederlands (Dutch)', value: 'nl-NL', display: 'NL' },
|
{ label: 'Deutsch', value: 'de-DE', display: 'DE' },
|
||||||
{ label: 'Deutsch (German)', value: 'de-DE', display: 'DE' },
|
{ label: 'Español', value: 'es-MX', display: 'ES' },
|
||||||
{ label: '日本語 (Japanese)', value: 'ja-JP', display: '日本語' },
|
{ label: '日本語', value: 'ja-JP', display: 'JP' },
|
||||||
{ label: 'Русский (Russian)', value: 'ru-RU', display: 'РУ' },
|
{ label: 'Nederlands', value: 'nl-NL', display: 'NL' },
|
||||||
|
{ label: 'Русский', value: 'ru-RU', display: 'RU' },
|
||||||
{ label: 'Turkish', value: 'tr-TR', display: 'TR' },
|
{ label: 'Turkish', value: 'tr-TR', display: 'TR' },
|
||||||
{ label: 'Español (Mexicano)', value: 'es-MX', display: 'ES' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function dateFormat(date, str, locale) {
|
export function dateFormat(date, str, locale) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "0.29.0",
|
"version": "0.32.0",
|
||||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -23,10 +23,10 @@
|
|||||||
"build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma",
|
"build-postgresql-schema": "dotenv prisma introspect -- --schema=./prisma/schema.postgresql.prisma",
|
||||||
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
|
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
|
||||||
"build-lang": "npm-run-all format-lang compile-lang",
|
"build-lang": "npm-run-all format-lang compile-lang",
|
||||||
"extract-lang": "formatjs extract {pages,components}/**/*.js --out-file lang/en-US.json",
|
"extract-lang": "formatjs extract {pages,components}/**/*.js --out-file build/messages.json",
|
||||||
"merge-lang": "node scripts/merge-lang.js",
|
"merge-lang": "node scripts/merge-lang.js",
|
||||||
"format-lang": "node scripts/format-lang.js",
|
"format-lang": "node scripts/format-lang.js",
|
||||||
"compile-lang": "formatjs compile-folder --ast lang-formatted lang-compiled"
|
"compile-lang": "formatjs compile-folder --ast build lang-compiled"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.js": [
|
"**/*.js": [
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Layout from 'components/layout/Layout';
|
import Layout from 'components/layout/Layout';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default function Custom404() {
|
export default function Custom404() {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
<h1>oops! page not found</h1>
|
<h1>
|
||||||
|
<FormattedMessage id="message.page-not-found" defaultMessage="Page not found" />
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Layout from 'components/layout/Layout';
|
|
||||||
import AccountSettings from 'components/settings/AccountSettings';
|
|
||||||
import useRequireLogin from 'hooks/useRequireLogin';
|
|
||||||
|
|
||||||
export default function AccountPage() {
|
|
||||||
const { loading } = useRequireLogin();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<AccountSettings />
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
@ -9,24 +9,20 @@ export default async (req, res) => {
|
|||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
const user_id = +id;
|
const user_id = +id;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (is_admin) {
|
||||||
if (is_admin) {
|
|
||||||
const account = await getAccountById(user_id);
|
|
||||||
|
|
||||||
return ok(res, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const account = await getAccountById(user_id);
|
||||||
|
|
||||||
|
return ok(res, account);
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
if (is_admin) {
|
await deleteAccount(user_id);
|
||||||
await deleteAccount(user_id);
|
|
||||||
|
|
||||||
return ok(res);
|
return ok(res);
|
||||||
}
|
|
||||||
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
return methodNotAllowed(res);
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { getAccountById, updateAccount } from 'lib/queries';
|
import { getAccountById, updateAccount } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { badRequest, methodNotAllowed, ok } from 'lib/response';
|
import { badRequest, methodNotAllowed, ok, unauthorized } from 'lib/response';
|
||||||
import { checkPassword, hashPassword } from 'lib/crypto';
|
import { checkPassword, hashPassword } from 'lib/crypto';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
const { user_id } = req.auth;
|
const { user_id, is_admin } = req.auth;
|
||||||
const { current_password, new_password } = req.body;
|
const { current_password, new_password } = req.body;
|
||||||
|
|
||||||
|
if (is_admin) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const account = await getAccountById(user_id);
|
const account = await getAccountById(user_id);
|
||||||
const valid = await checkPassword(current_password, account.password);
|
const valid = await checkPassword(current_password, account.password);
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import { getActiveVisitors } from 'lib/queries';
|
import { getActiveVisitors } from 'lib/queries';
|
||||||
import { ok } from 'lib/response';
|
import { methodNotAllowed, ok } from 'lib/response';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id } = req.query;
|
await useAuth(req, res);
|
||||||
const website_id = +id;
|
|
||||||
|
|
||||||
const result = await getActiveVisitors(website_id);
|
if (req.method === 'GET') {
|
||||||
|
const { id } = req.query;
|
||||||
|
const website_id = +id;
|
||||||
|
|
||||||
return ok(res, result);
|
const result = await getActiveVisitors(website_id);
|
||||||
|
|
||||||
|
return ok(res, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { getEvents } from 'lib/queries';
|
import { getEvents } from 'lib/queries';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest, methodNotAllowed } from 'lib/response';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
const unitTypes = ['month', 'hour', 'day'];
|
const unitTypes = ['year', 'month', 'hour', 'day'];
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id, start_at, end_at, unit, tz } = req.query;
|
await useAuth(req, res);
|
||||||
|
|
||||||
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
if (req.method === 'GET') {
|
||||||
return badRequest(res);
|
const { id, start_at, end_at, unit, tz } = req.query;
|
||||||
|
|
||||||
|
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
||||||
|
return badRequest(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const websiteId = +id;
|
||||||
|
const startDate = new Date(+start_at);
|
||||||
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
|
const events = await getEvents(websiteId, startDate, endDate, tz, unit);
|
||||||
|
|
||||||
|
return ok(res, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
const websiteId = +id;
|
return methodNotAllowed(res);
|
||||||
const startDate = new Date(+start_at);
|
|
||||||
const endDate = new Date(+end_at);
|
|
||||||
|
|
||||||
const events = await getEvents(websiteId, startDate, endDate, tz, unit);
|
|
||||||
|
|
||||||
return ok(res, events);
|
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
import { getMetrics } from 'lib/queries';
|
import { getMetrics } from 'lib/queries';
|
||||||
import { ok } from 'lib/response';
|
import { methodNotAllowed, ok } from 'lib/response';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id, start_at, end_at } = req.query;
|
await useAuth(req, res);
|
||||||
const websiteId = +id;
|
|
||||||
const startDate = new Date(+start_at);
|
|
||||||
const endDate = new Date(+end_at);
|
|
||||||
|
|
||||||
const metrics = await getMetrics(websiteId, startDate, endDate);
|
if (req.method === 'GET') {
|
||||||
|
const { id, start_at, end_at } = req.query;
|
||||||
|
const websiteId = +id;
|
||||||
|
const startDate = new Date(+start_at);
|
||||||
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
const metrics = await getMetrics(websiteId, startDate, endDate);
|
||||||
obj[key] = Number(metrics[0][key]) || 0;
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return ok(res, stats);
|
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
||||||
|
obj[key] = Number(metrics[0][key]) || 0;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return ok(res, stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
};
|
};
|
||||||
|
@ -1,24 +1,31 @@
|
|||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { getPageviews } from 'lib/queries';
|
import { getPageviews } from 'lib/queries';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest, methodNotAllowed } from 'lib/response';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
const unitTypes = ['month', 'hour', 'day'];
|
const unitTypes = ['year', 'month', 'hour', 'day'];
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id, start_at, end_at, unit, tz } = req.query;
|
await useAuth(req, res);
|
||||||
|
|
||||||
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
if (req.method === 'GET') {
|
||||||
return badRequest(res);
|
const { id, start_at, end_at, unit, tz } = req.query;
|
||||||
|
|
||||||
|
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
||||||
|
return badRequest(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const websiteId = +id;
|
||||||
|
const startDate = new Date(+start_at);
|
||||||
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
|
const [pageviews, uniques] = await Promise.all([
|
||||||
|
getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
|
||||||
|
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ok(res, { pageviews, uniques });
|
||||||
}
|
}
|
||||||
|
|
||||||
const websiteId = +id;
|
return methodNotAllowed(res);
|
||||||
const startDate = new Date(+start_at);
|
|
||||||
const endDate = new Date(+end_at);
|
|
||||||
|
|
||||||
const [pageviews, uniques] = await Promise.all([
|
|
||||||
getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
|
|
||||||
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return ok(res, { pageviews, uniques });
|
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { getRankings } from 'lib/queries';
|
import { getRankings } from 'lib/queries';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest, methodNotAllowed } from 'lib/response';
|
||||||
import { DOMAIN_REGEX } from '../../../../lib/constants';
|
import { DOMAIN_REGEX } from 'lib/constants';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
const sessionColumns = ['browser', 'os', 'device', 'country'];
|
const sessionColumns = ['browser', 'os', 'device', 'country'];
|
||||||
const pageviewColumns = ['url', 'referrer'];
|
const pageviewColumns = ['url', 'referrer'];
|
||||||
@ -25,29 +26,35 @@ function getColumn(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id, type, start_at, end_at, domain } = req.query;
|
await useAuth(req, res);
|
||||||
const websiteId = +id;
|
|
||||||
const startDate = new Date(+start_at);
|
|
||||||
const endDate = new Date(+end_at);
|
|
||||||
|
|
||||||
if (
|
if (req.method === 'GET') {
|
||||||
type !== 'event' &&
|
const { id, type, start_at, end_at, domain } = req.query;
|
||||||
!sessionColumns.includes(type) &&
|
const websiteId = +id;
|
||||||
!pageviewColumns.includes(type) &&
|
const startDate = new Date(+start_at);
|
||||||
domain &&
|
const endDate = new Date(+end_at);
|
||||||
DOMAIN_REGEX.test(domain)
|
|
||||||
) {
|
if (
|
||||||
return badRequest(res);
|
type !== 'event' &&
|
||||||
|
!sessionColumns.includes(type) &&
|
||||||
|
!pageviewColumns.includes(type) &&
|
||||||
|
domain &&
|
||||||
|
DOMAIN_REGEX.test(domain)
|
||||||
|
) {
|
||||||
|
return badRequest(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankings = await getRankings(
|
||||||
|
websiteId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
getColumn(type),
|
||||||
|
getTable(type),
|
||||||
|
domain,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ok(res, rankings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rankings = await getRankings(
|
return methodNotAllowed(res);
|
||||||
websiteId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
getColumn(type),
|
|
||||||
getTable(type),
|
|
||||||
domain,
|
|
||||||
);
|
|
||||||
|
|
||||||
return ok(res, rankings);
|
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import { getUserWebsites } from 'lib/queries';
|
import { getUserWebsites } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, methodNotAllowed } from 'lib/response';
|
import { ok, methodNotAllowed, unauthorized } from 'lib/response';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
const { user_id } = req.auth;
|
const { user_id, is_admin } = req.auth;
|
||||||
|
const { userId } = req.query;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const websites = await getUserWebsites(user_id);
|
if (userId && !is_admin) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const websites = await getUserWebsites(+userId || user_id);
|
||||||
|
|
||||||
return ok(res, websites);
|
return ok(res, websites);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import Layout from 'components/layout/Layout';
|
import Layout from 'components/layout/Layout';
|
||||||
import WebsiteList from 'components/WebsiteList';
|
import WebsiteList from 'components/WebsiteList';
|
||||||
import useRequireLogin from 'hooks/useRequireLogin';
|
import useRequireLogin from 'hooks/useRequireLogin';
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { loading } = useRequireLogin();
|
const { loading } = useRequireLogin();
|
||||||
|
const router = useRouter();
|
||||||
|
const { id } = router.query;
|
||||||
|
const userId = id?.[0];
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null;
|
return null;
|
||||||
@ -12,7 +16,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<WebsiteList />
|
<WebsiteList userId={userId} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ const path = require('path');
|
|||||||
const prettier = require('prettier');
|
const prettier = require('prettier');
|
||||||
|
|
||||||
const src = path.resolve(__dirname, '../lang');
|
const src = path.resolve(__dirname, '../lang');
|
||||||
const dest = path.resolve(__dirname, '../lang-formatted');
|
const dest = path.resolve(__dirname, '../build');
|
||||||
const files = fs.readdirSync(src);
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
if (!fs.existsSync(dest)) {
|
if (!fs.existsSync(dest)) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const prettier = require('prettier');
|
const prettier = require('prettier');
|
||||||
const root = require('../lang/en-US.json');
|
const messages = require('../build/messages.json');
|
||||||
|
|
||||||
const dir = path.resolve(__dirname, '../lang');
|
const dest = path.resolve(__dirname, '../lang');
|
||||||
const files = fs.readdirSync(dir);
|
const files = fs.readdirSync(dest);
|
||||||
const keys = Object.keys(root).sort();
|
const keys = Object.keys(messages).sort();
|
||||||
|
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
const lang = require(`../lang/${file}`);
|
const lang = require(`../lang/${file}`);
|
||||||
@ -15,7 +15,7 @@ files.forEach(file => {
|
|||||||
const merged = keys.reduce((obj, key) => {
|
const merged = keys.reduce((obj, key) => {
|
||||||
const message = lang[key];
|
const message = lang[key];
|
||||||
|
|
||||||
obj[key] = message || root[key];
|
obj[key] = message || messages[key].defaultMessage;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
console.log(`* Added key ${key}`);
|
console.log(`* Added key ${key}`);
|
||||||
@ -26,5 +26,5 @@ files.forEach(file => {
|
|||||||
|
|
||||||
const json = prettier.format(JSON.stringify(merged), { parser: 'json' });
|
const json = prettier.format(JSON.stringify(merged), { parser: 'json' });
|
||||||
|
|
||||||
fs.writeFileSync(path.resolve(dir, file), json);
|
fs.writeFileSync(path.resolve(dest, file), json);
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,10 @@ body {
|
|||||||
|
|
||||||
.zh-CN {
|
.zh-CN {
|
||||||
font-family: 'Noto Sans SC', sans-serif !important;
|
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%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#__modals {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
|
Loading…
Reference in New Issue
Block a user