More refactoring.

This commit is contained in:
Mike Cao 2023-01-30 21:44:07 -08:00
parent 5f15ad0807
commit 02a1438cfe
41 changed files with 196 additions and 721 deletions

View file

@ -1,280 +0,0 @@
import { 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, Icon, Icons } from 'react-basics';
import { chunkArray } from 'next-basics';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date';
import { getDateLocale } from 'lib/lang';
import styles from './Calendar.module.css';
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} size="small">
{selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
</Icon>
</div>
<div
className={classNames(styles.selector, { [styles.open]: selectYear })}
onClick={toggleYearSelect}
>
{year}
<Icon className={styles.icon} size="small">
{selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
</Icon>
</div>
</div>
<div className={styles.body}>
{!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>
</div>
);
}
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
const dateLocale = getDateLocale(locale);
const weekStartsOn = dateLocale?.options?.weekStartsOn || 0;
const startWeek = startOfWeek(date, {
locale: dateLocale,
weekStartsOn,
});
const startMonth = startOfMonth(date);
const startDay = subDays(startMonth, startMonth.getDay() - weekStartsOn);
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>
{chunkArray(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>
{chunkArray(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}>
<div className={styles.left}>
<Button
size="small"
onClick={handlePrevClick}
disabled={years[0] <= minYear}
variant="light"
>
<Icon>
<Icons.ChevronDown />
</Icon>
</Button>
</div>
<div className={styles.middle}>
<table>
<tbody>
{chunkArray(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>
</div>
<div className={styles.right}>
<Button
size="small"
onClick={handleNextClick}
disabled={years[years.length - 1] > maxYear}
variant="light"
>
<Icon>
<Icons.ChevronDown />
</Icon>
</Button>
</div>
</div>
);
};

View file

@ -1,110 +0,0 @@
.calendar {
display: flex;
flex-direction: column;
flex: 1;
min-height: 306px;
}
.calendar table {
width: 100%;
border-spacing: 5px;
}
.calendar td {
color: var(--base800);
cursor: pointer;
text-align: center;
vertical-align: center;
height: 40px;
width: 40px;
border-radius: 5px;
border: 1px solid transparent;
}
.calendar td:hover {
border: 1px solid var(--base300);
background: var(--base75);
}
.calendar td.faded {
color: var(--base500);
}
.calendar td.selected {
font-weight: 600;
border: 1px solid var(--base600);
}
.calendar td.selected:hover {
background: transparent;
}
.calendar td.disabled {
color: var(--base400);
background: var(--base75);
}
.calendar td.disabled:hover {
cursor: default;
background: var(--base75);
border-color: transparent;
}
.calendar td.faded.disabled {
background: var(--base100);
}
.header {
display: flex;
justify-content: space-evenly;
align-items: center;
font-weight: 700;
line-height: 40px;
font-size: var(--font-size-md);
}
.body {
display: flex;
}
.selector {
cursor: pointer;
}
.pager {
display: flex;
flex: 1;
}
.pager button {
align-self: center;
}
.middle {
flex: 1;
}
.left,
.right {
display: flex;
justify-content: center;
align-items: center;
}
.left svg {
transform: rotate(90deg);
}
.right svg {
transform: rotate(-90deg);
}
.icon {
margin-left: 10px;
}
@media only screen and (max-width: 992px) {
.calendar table {
max-width: calc(100vw - 30px);
}
}

View file

@ -1,37 +0,0 @@
import { useState } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-basics';
import { FormattedMessage } from 'react-intl';
const defaultText = (
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
);
function CopyButton({ element, ...props }) {
const [text, setText] = useState(defaultText);
function handleClick() {
if (element?.current) {
element.current.select();
document.execCommand('copy');
setText(<FormattedMessage id="message.copied" defaultMessage="Copied!" />);
window.getSelection().removeAllRanges();
}
}
return (
<Button {...props} onClick={handleClick}>
{text}
</Button>
);
}
CopyButton.propTypes = {
element: PropTypes.shape({
current: PropTypes.shape({
select: PropTypes.func.isRequired,
}),
}),
};
export default CopyButton;

View file

@ -1,66 +0,0 @@
import { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Menu from './Menu';
import useDocumentClick from 'hooks/useDocumentClick';
import Chevron from 'assets/chevron-down.svg';
import styles from './Dropdown.module.css';
import { Icon } from 'react-basics';
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
const [showMenu, setShowMenu] = useState(false);
const ref = useRef();
const selectedOption = options.find(e => e.value === value);
function handleShowMenu() {
setShowMenu(state => !state);
}
function handleSelect(selected, e) {
e.stopPropagation();
setShowMenu(false);
onChange(selected);
}
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}>
<div className={styles.text}>{options.find(e => e.value === value)?.label || value}</div>
<Icon className={styles.icon} size="small">
<Chevron />
</Icon>
</div>
{showMenu && (
<Menu
className={menuClassName}
options={options}
selectedOption={selectedOption}
onSelect={handleSelect}
float="bottom"
/>
)}
</div>
);
}
DropDown.propTypes = {
value: PropTypes.any,
className: PropTypes.string,
menuClassName: PropTypes.string,
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.any.isRequired,
label: PropTypes.node,
}),
),
onChange: PropTypes.func,
};
export default DropDown;

View file

@ -1,27 +0,0 @@
.dropdown {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--base500);
border-radius: 4px;
cursor: pointer;
}
.value {
flex: 1;
display: flex;
justify-content: space-between;
flex-wrap: nowrap;
white-space: nowrap;
padding: 4px 16px;
min-width: 160px;
}
.text {
flex: 1;
}
.icon {
padding-left: 20px;
}

View file

@ -2,8 +2,7 @@ import classNames from 'classnames';
import Link from 'next/link';
import { safeDecodeURI } from 'next-basics';
import usePageQuery from 'hooks/usePageQuery';
import External from 'assets/arrow-up-right-from-square.svg';
import { Icon } from 'react-basics';
import { Icon, Icons } from 'react-basics';
import styles from './FilterLink.module.css';
export default function FilterLink({ id, value, label, externalUrl }) {
@ -26,7 +25,7 @@ export default function FilterLink({ id, value, label, externalUrl }) {
{externalUrl && (
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
<Icon className={styles.icon}>
<External />
<Icons.External />
</Icon>
</a>
)}

View file

@ -1,10 +1,9 @@
import { Button, Icon } from 'react-basics';
import XMark from 'assets/xmark.svg';
import Bars from 'assets/bars.svg';
import { useState } from 'react';
import styles from './HamburgerButton.module.css';
import MobileMenu from './MobileMenu';
import { FormattedMessage } from 'react-intl';
import MobileMenu from './MobileMenu';
import Icons from 'components/icons';
import styles from './HamburgerButton.module.css';
const menuItems = [
{
@ -37,7 +36,7 @@ export default function HamburgerButton() {
return (
<>
<Button className={styles.button} onClick={handleClick}>
<Icon>{active ? <XMark /> : <Bars />}</Icon>
<Icon>{active ? <Icons.Close /> : <Icons.Menu />}</Icon>
</Button>
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
</>

View file

@ -1,19 +1,25 @@
import classNames from 'classnames';
import Link from './Link';
import { Button } from 'react-basics';
import XMark from 'assets/xmark.svg';
import Link from 'next/link';
import { Button, Icon } from 'react-basics';
import Icons from 'components/icons';
import styles from './MobileMenu.module.css';
export default function MobileMenu({ items = [], onClose }) {
return (
<div className={classNames(styles.menu, 'container')}>
<div className={classNames(styles.menu)}>
<div className={styles.header}>
<Button icon={<XMark />} onClick={onClose} />
<Button onClick={onClose}>
<Icon>
<Icons.Close />
</Icon>
</Button>
</div>
<div className={styles.items}>
{items.map(({ label, value }) => (
<Link key={value} href={value} className={styles.item} onClick={onClose}>
{label}
<Link key={value} href={value}>
<a className={styles.item} onClick={onClose}>
{label}
</a>
</Link>
))}
</div>

View file

@ -1,12 +1,10 @@
import { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import useStore from 'store/queries';
import { setDateRange } from 'store/websites';
import { Button, Icon } from 'react-basics';
import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import useDateRange from 'hooks/useDateRange';
import Icons from 'components/icons';
function RefreshButton({ websiteId }) {
const [dateRange] = useDateRange(websiteId);
@ -17,7 +15,7 @@ function RefreshButton({ websiteId }) {
function handleClick() {
if (!loading && dateRange) {
setLoading(true);
if (/^[\d]+/.test(dateRange.value)) {
if (/^\d+/.test(dateRange.value)) {
setDateRange(websiteId, dateRange.value);
} else {
setDateRange(websiteId, dateRange);
@ -36,13 +34,11 @@ function RefreshButton({ websiteId }) {
size="small"
onClick={handleClick}
>
<Icon>{loading ? <Dots /> : <Refresh />}</Icon>
<Icon>
<Icons.Refresh />
</Icon>
</Button>
);
}
RefreshButton.propTypes = {
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default RefreshButton;