mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Website edit functionality.
This commit is contained in:
parent
30b87bc4c4
commit
00e232fee8
63 changed files with 301 additions and 94 deletions
24
components/common/DateFilter.js
Normal file
24
components/common/DateFilter.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import DropDown from './DropDown';
|
||||
|
||||
const filterOptions = [
|
||||
{ label: 'Last 24 hours', value: '24hour' },
|
||||
{ label: 'Last 7 days', value: '7day' },
|
||||
{ label: 'Last 30 days', value: '30day' },
|
||||
{ label: 'Last 90 days', value: '90day' },
|
||||
{ label: 'Today', value: '1day' },
|
||||
{ label: 'This week', value: '1week' },
|
||||
{ label: 'This month', value: '1month' },
|
||||
{ label: 'This year', value: '1year' },
|
||||
];
|
||||
|
||||
export default function DateFilter({ value, onChange, className }) {
|
||||
function handleChange(value) {
|
||||
onChange(getDateRange(value));
|
||||
}
|
||||
|
||||
return (
|
||||
<DropDown className={className} value={value} options={filterOptions} onChange={handleChange} />
|
||||
);
|
||||
}
|
||||
44
components/common/DropDown.js
Normal file
44
components/common/DropDown.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Menu from '../interface/Menu';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
import Chevron from 'assets/chevron-down.svg';
|
||||
import styles from './Dropdown.module.css';
|
||||
import Icon from '../interface/Icon';
|
||||
|
||||
export default function DropDown({
|
||||
value,
|
||||
className,
|
||||
menuClassName,
|
||||
options = [],
|
||||
onChange = () => {},
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const ref = useRef();
|
||||
|
||||
function handleShowMenu() {
|
||||
setShowMenu(state => !state);
|
||||
}
|
||||
|
||||
function handleSelect(value, e) {
|
||||
e.stopPropagation();
|
||||
setShowMenu(false);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current.contains(e.target)) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
||||
<div className={styles.value}>
|
||||
{options.find(e => e.value === value)?.label}
|
||||
<Icon icon={<Chevron />} size="S" className={styles.icon} />
|
||||
</div>
|
||||
{showMenu && <Menu className={menuClassName} options={options} onSelect={handleSelect} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
components/common/Dropdown.module.css
Normal file
22
components/common/Dropdown.module.css
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
.dropdown {
|
||||
position: relative;
|
||||
font-size: var(--font-size-small);
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.value {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
padding: 4px 32px 4px 16px;
|
||||
border: 1px solid var(--gray500);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 12px;
|
||||
margin: auto;
|
||||
}
|
||||
16
components/common/Modal.js
Normal file
16
components/common/Modal.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import styles from './Modal.module.css';
|
||||
|
||||
export default function Modal({ title, children }) {
|
||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||
|
||||
return (
|
||||
<animated.div className={styles.modal} style={props}>
|
||||
<div className={styles.content}>
|
||||
{title && <div className={styles.header}>{title}</div>}
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
</animated.div>
|
||||
);
|
||||
}
|
||||
45
components/common/Modal.module.css
Normal file
45
components/common/Modal.module.css
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
.modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
background: var(--gray900);
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--gray50);
|
||||
min-width: 200px;
|
||||
min-height: 100px;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--gray300);
|
||||
padding: 30px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
36
components/common/Table.js
Normal file
36
components/common/Table.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Table.module.css';
|
||||
|
||||
export default function Table({ columns, rows }) {
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
<div className={styles.header}>
|
||||
{columns.map(({ key, label, header }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(styles.head, header?.className)}
|
||||
style={header?.style}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div className={styles.row} key={rowIndex}>
|
||||
{columns.map(({ key, render, cell }) => (
|
||||
<div
|
||||
key={`${rowIndex}${key}`}
|
||||
className={classNames(styles.cell, cell?.className)}
|
||||
style={cell?.style}
|
||||
>
|
||||
{render ? render(row) : row[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
components/common/Table.module.css
Normal file
32
components/common/Table.module.css
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.head {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
line-height: 40px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
71
components/common/WorldMap.js
Normal file
71
components/common/WorldMap.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useState } from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import classNames from 'classnames';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||
import styles from './WorldMap.module.css';
|
||||
|
||||
const geoUrl = '/world-110m.json';
|
||||
|
||||
export default function WorldMap({
|
||||
data,
|
||||
className,
|
||||
baseColor = '#e9f3fd',
|
||||
fillColor = '#f5f5f5',
|
||||
strokeColor = '#2680eb',
|
||||
hoverColor = '#2680eb',
|
||||
}) {
|
||||
const [tooltip, setTooltip] = useState();
|
||||
|
||||
function getFillColor(code) {
|
||||
if (code === 'AQ') return '#ffffff';
|
||||
const country = data?.find(({ x }) => x === code);
|
||||
return country ? tinycolor(baseColor).darken(country.z) : fillColor;
|
||||
}
|
||||
|
||||
function getStrokeColor(code) {
|
||||
return code === 'AQ' ? '#ffffff' : strokeColor;
|
||||
}
|
||||
|
||||
function getHoverColor(code) {
|
||||
return code === 'AQ' ? '#ffffff' : hoverColor;
|
||||
}
|
||||
|
||||
function handleHover({ ISO_A2: code, NAME: name }) {
|
||||
const country = data?.find(({ x }) => x === code);
|
||||
setTooltip(`${name}: ${country?.y || 0} visitors`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<ComposableMap data-tip="" projection="geoMercator">
|
||||
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
|
||||
<Geographies geography={geoUrl}>
|
||||
{({ geographies }) => {
|
||||
return geographies.map(geo => {
|
||||
const code = geo.properties.ISO_A2;
|
||||
|
||||
return (
|
||||
<Geography
|
||||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
fill={getFillColor(code)}
|
||||
stroke={getStrokeColor(code)}
|
||||
style={{
|
||||
default: { outline: 'none' },
|
||||
hover: { outline: 'none', fill: getHoverColor(code) },
|
||||
pressed: { outline: 'none' },
|
||||
}}
|
||||
onMouseOver={() => handleHover(geo.properties)}
|
||||
onMouseOut={() => setTooltip(null)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}}
|
||||
</Geographies>
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
<ReactTooltip>{tooltip}</ReactTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
components/common/WorldMap.module.css
Normal file
5
components/common/WorldMap.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue