Skip to content

Commit 529c896

Browse files
author
k.golikov
committed
Add CounterPage
1 parent bfbc5a0 commit 529c896

File tree

11 files changed

+254
-4
lines changed

11 files changed

+254
-4
lines changed

src/components/flex/Flex.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const Flex: FunctionComponent<Props> = (props) => {
4444
return defaults(
4545
{
4646
display: 'flex',
47-
flexDirection: () => {
47+
flexDirection: (() => {
4848
switch (true) {
4949
case row:
5050
return 'row';
@@ -53,7 +53,7 @@ const Flex: FunctionComponent<Props> = (props) => {
5353
default:
5454
return direction;
5555
}
56-
},
56+
})(),
5757
justifyContent: justify,
5858
alignItems: align,
5959
alignContent,

src/constants/router/menuItems.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const menuItems: MenuItem[] = [
7070
{
7171
route: routes.dateUtils
7272
},
73+
{
74+
route: routes.counter
75+
},
7376
{
7477
route: routes.jsonToYaml,
7578
title: 'JSON to YAML converter',

src/constants/router/routes.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import DataUrlPage from '../../pages/dataUrlPage/DataUrlPage';
3030
import DataUrlViewPage from '../../pages/dataUrlViewPage/DataUrlViewPage';
3131
import RouteContextInitializer from '../../layouts/RouteContextInitializer';
3232
import JsonToYamlPage from '../../pages/jsonToYamlPage/JsonToYamlPage';
33+
import CounterPage from '../../pages/counterPage/CounterPage';
3334

3435
export interface AppRoute extends Omit<RouteProps, 'element'> {
3536
path: string;
@@ -64,6 +65,7 @@ type AppRoutesMap = Readonly<{
6465
base64: AppRoute;
6566
dataUrl: AppRoute;
6667
dataUrlView: AppRoute;
68+
counter: AppRoute;
6769
settings: AppRoute;
6870
about: AppRoute;
6971
}>;
@@ -195,6 +197,11 @@ export const routes: AppRoutesMap = {
195197
title: 'Content View',
196198
isLayoutHidden: true
197199
},
200+
counter: {
201+
path: '/tools/counter',
202+
component: CounterPage,
203+
title: 'Counter'
204+
},
198205
settings: {
199206
path: '/settings',
200207
component: SettingsPage,

src/hooks/useArrayStateMutator.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Dispatch, SetStateAction, useMemo } from 'react';
2+
import useStateProducer from './useStateProducer';
3+
4+
const useArrayStateMutator = <T>(setState: Dispatch<SetStateAction<T[]>>) => {
5+
const produceState = useStateProducer(setState);
6+
7+
return useMemo(() => {
8+
const add = (...items: T[]) => setState((state) => [...state, ...items]);
9+
const changeByIndex = (index: number, newItem: T) =>
10+
produceState((state) => {
11+
state[index] = newItem;
12+
});
13+
const updateByIndex = (index: number, newItem: (oldItem: T) => T) =>
14+
produceState((state) => {
15+
const oldItem = state[index];
16+
if (oldItem === undefined) {
17+
return;
18+
}
19+
20+
changeByIndex(index, newItem(oldItem));
21+
});
22+
const removeByIndex = (index: number) =>
23+
produceState((state) => {
24+
state.splice(index, 1);
25+
});
26+
const fpChangeByIndex = (index: number) => (newItem: T) => changeByIndex(index, newItem);
27+
const fpUpdateByIndex = (index: number) => (newItem: (oldItem: T) => T) => updateByIndex(index, newItem);
28+
const fpRemoveByIndex = (index: number) => () => removeByIndex(index);
29+
const clear = () => setState([]);
30+
31+
return {
32+
add,
33+
changeByIndex,
34+
updateByIndex,
35+
removeByIndex,
36+
fpChangeByIndex,
37+
fpUpdateByIndex,
38+
fpRemoveByIndex,
39+
clear
40+
};
41+
}, [setState]);
42+
};
43+
44+
export default useArrayStateMutator;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
2+
import { useLocalstorageState } from 'rooks';
3+
4+
type UseLocalstorageStateReturnValue<S> = [S, Dispatch<SetStateAction<S>>, () => void];
5+
6+
const useWriteableLocalstorageState = <S>(
7+
key: string,
8+
initialState?: S | (() => S)
9+
): UseLocalstorageStateReturnValue<S> => {
10+
const [lsState, setLsState, resetLsState] = useLocalstorageState<S>(key, initialState);
11+
const [state, setState] = useState<S>(lsState);
12+
13+
useEffect(() => {
14+
setLsState(state as S);
15+
}, [state]);
16+
17+
return useMemo(() => [state, setState, resetLsState], [state, setState, resetLsState]);
18+
};
19+
20+
export default useWriteableLocalstorageState;

src/pages/counterPage/CounterPage.module.scss

Whitespace-only changes.

src/pages/counterPage/CounterPage.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { FunctionComponent, useCallback } from 'react';
2+
import PageContainer from '../../layouts/pages/pageContainer/PageContainer';
3+
import getLocalStorageKey from '../../utils/getLocalStorageKey';
4+
import useArrayStateMutator from '../../hooks/useArrayStateMutator';
5+
import { Counter } from './types';
6+
import CounterItem from './components/counterItem/CounterItem';
7+
import { Button, Modal } from 'antd';
8+
import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
9+
import Flex from '../../components/flex/Flex';
10+
import useWriteableLocalstorageState from '../../hooks/useWriteableLocalstorageState';
11+
12+
const createCounter = (name = '', count = 0): Counter => ({
13+
name: '',
14+
count: 0
15+
});
16+
17+
const initialCounters: Counter[] = [createCounter('Count')];
18+
19+
const CounterPage: FunctionComponent = () => {
20+
const [counters, setCounters] = useWriteableLocalstorageState<Counter[]>(
21+
getLocalStorageKey('counter', 'counters'),
22+
initialCounters
23+
);
24+
const {
25+
fpChangeByIndex: handleItemChange,
26+
fpRemoveByIndex: handleItemRemove,
27+
add: addItem,
28+
clear: clearItems
29+
} = useArrayStateMutator(setCounters);
30+
31+
const handleAddItem = useCallback(() => {
32+
addItem(createCounter());
33+
}, [addItem]);
34+
35+
const handleClear = useCallback(() => {
36+
Modal.warn({
37+
title: 'Clear counters',
38+
content: 'Are you sure you want to delete all counters?',
39+
onOk: clearItems,
40+
okCancel: true
41+
});
42+
}, [clearItems]);
43+
44+
return (
45+
<PageContainer title="Counters">
46+
<Flex column gap={8}>
47+
{counters.length > 0 && (
48+
<>
49+
<Flex column>
50+
<Button icon={<CloseOutlined />} size="large" onClick={handleClear}>
51+
Clear counters list
52+
</Button>
53+
</Flex>
54+
<Flex column gap={8}>
55+
{counters.map((counter, index) => (
56+
<CounterItem
57+
key={index}
58+
counter={counter}
59+
onChange={handleItemChange(index)}
60+
onRemove={handleItemRemove(index)}
61+
/>
62+
))}
63+
</Flex>
64+
</>
65+
)}
66+
<Button icon={<PlusOutlined />} type="dashed" onClick={handleAddItem} size="large">
67+
Add counter
68+
</Button>
69+
</Flex>
70+
</PageContainer>
71+
);
72+
};
73+
74+
export default CounterPage;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@import '../../../../styles/main';
2+
3+
.input {
4+
border-end-end-radius: 0 !important;
5+
border-start-end-radius: 0 !important;
6+
margin-right: -1px;
7+
z-index: 1;
8+
flex: 1;
9+
}
10+
11+
.firstButton {
12+
border-start-start-radius: 0 !important;
13+
border-end-start-radius: 0 !important;
14+
z-index: 0;
15+
}
16+
17+
.counter {
18+
padding: 0 5px;
19+
min-width: 40px;
20+
//cursor: default !important;
21+
22+
color: black !important;
23+
@include themeDark {
24+
color: white !important;
25+
}
26+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { ChangeEvent, FunctionComponent, useState } from 'react';
2+
import { Counter } from '../../types';
3+
import { Button, Input, Popover } from 'antd';
4+
import { CloseOutlined, MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
5+
import Flex from '../../../../components/flex/Flex';
6+
import ButtonGroup from 'antd/lib/button/button-group';
7+
import styles from './CounterItem.module.scss';
8+
import classNames from 'classnames';
9+
10+
interface Props {
11+
counter: Counter;
12+
onChange: (newCounter: Counter) => void;
13+
onRemove: () => void;
14+
}
15+
16+
const CounterItem: FunctionComponent<Props> = ({ counter, onChange, onRemove }) => {
17+
const [isPopoverVisible, setIsPopoverVisible] = useState<boolean>(false);
18+
19+
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
20+
onChange({
21+
...counter,
22+
name: event.target.value
23+
});
24+
};
25+
26+
const handleCountIncrement = (increment: number) => () => {
27+
handleCountChange(counter.count + increment)();
28+
};
29+
30+
const handleCountChange = (count: number) => () => {
31+
onChange({
32+
...counter,
33+
count
34+
});
35+
};
36+
37+
const handlePopoverClick = (callback: () => void) => () => {
38+
setIsPopoverVisible(false);
39+
callback();
40+
};
41+
42+
return (
43+
<Flex row>
44+
<Input value={counter.name} onChange={handleNameChange} className={styles.input} />
45+
<ButtonGroup>
46+
<Popover
47+
placement="bottomRight"
48+
content={
49+
<Flex column gap={8}>
50+
<Button icon={<ReloadOutlined />} onClick={handlePopoverClick(handleCountChange(0))}>
51+
Reset
52+
</Button>
53+
<Button icon={<CloseOutlined />} onClick={handlePopoverClick(onRemove)}>
54+
Remove
55+
</Button>
56+
</Flex>
57+
}
58+
visible={isPopoverVisible}
59+
onVisibleChange={setIsPopoverVisible}
60+
>
61+
<Button size="large" className={classNames(styles.firstButton, styles.counter)}>
62+
{counter.count}
63+
</Button>
64+
</Popover>
65+
<Button icon={<MinusOutlined />} size="large" onClick={handleCountIncrement(-1)} />
66+
<Button icon={<PlusOutlined />} size="large" onClick={handleCountIncrement(1)} />
67+
</ButtonGroup>
68+
</Flex>
69+
);
70+
};
71+
72+
export default CounterItem;

src/pages/counterPage/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface Counter {
2+
name: string;
3+
count: number;
4+
}

0 commit comments

Comments
 (0)