Skip to content

Commit c4fdf0e

Browse files
committed
feat(widgets): add calculator widget
1 parent b26f0c1 commit c4fdf0e

File tree

29 files changed

+1078
-371
lines changed

29 files changed

+1078
-371
lines changed

.tx/config

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,9 @@ file_filter = src/utils/i18n/locales/<lang>/wallet.json
6060
source_file = src/utils/i18n/locales/en/wallet.json
6161
source_lang = en
6262
type = STRUCTURED_JSON
63+
64+
[o:synonym:p:bitkit:r:widgets]
65+
file_filter = src/utils/i18n/locales/<lang>/widgets.json
66+
source_file = src/utils/i18n/locales/en/widgets.json
67+
source_lang = en
68+
type = STRUCTURED_JSON

src/assets/icons/widgets.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,15 @@
11
export const bitfinexIcon = (): string =>
22
'<svg fill="none" height="40" viewBox="0 0 40 40" width="40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m10 10h20v20h-20z"/></clipPath><path d="m0 0h40v40h-40z" fill="#0e3452"/><g clip-path="url(#a)" fill="#03ca9b"><path d="m10.1704 22.995c-.17231-2.788.9987-5.887 3.4105-8.2988 5.2443-5.24423 16.1656-4.5092 16.2278-4.5049-.0299.0432-8.0138 11.619-17.4749 12.7017-.7314.0837-1.4542.1166-2.1634.102z"/><path d="m11.2784 26.5596c.2701.4307.5894.8341.9588 1.2034 3.2372 3.2372 9.0874 2.6356 13.0668-1.3438 5.2591-5.2592 4.5047-16.2279 4.5047-16.2279-.0291.065-5.7521 12.8938-14.8479 15.692-1.2347.3799-2.4724.6012-3.6824.6763z"/></g></svg>';
3+
4+
export const calculatorIcon = (): string =>
5+
`<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
6+
<rect width="64" height="64" rx="8" fill="#FF4400"/>
7+
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 40.75C17 40.0596 17.5596 39.5 18.25 39.5H28.25C28.9404 39.5 29.5 40.0596 29.5 40.75C29.5 41.4404 28.9404 42 28.25 42H18.25C17.5596 42 17 41.4404 17 40.75Z" fill="white"/>
8+
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.25 34.5C23.9404 34.5 24.5 35.0596 24.5 35.75V45.75C24.5 46.4404 23.9404 47 23.25 47C22.5596 47 22 46.4404 22 45.75V35.75C22 35.0596 22.5596 34.5 23.25 34.5Z" fill="white"/>
9+
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 23.25C17 22.5596 17.5596 22 18.25 22H28.25C28.9404 22 29.5 22.5596 29.5 23.25C29.5 23.9404 28.9404 24.5 28.25 24.5H18.25C17.5596 24.5 17 23.9404 17 23.25Z" fill="white"/>
10+
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.5 38.2607C34.5 37.5704 35.0596 37.0107 35.75 37.0107H45.75C46.4404 37.0107 47 37.5704 47 38.2607C47 38.9511 46.4404 39.5107 45.75 39.5107H35.75C35.0596 39.5107 34.5 38.9511 34.5 38.2607Z" fill="white"/>
11+
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.5 43.2393C34.5 42.5489 35.0596 41.9893 35.75 41.9893H45.75C46.4404 41.9893 47 42.5489 47 43.2393C47 43.9296 46.4404 44.4893 45.75 44.4893H35.75C35.0596 44.4893 34.5 43.9296 34.5 43.2393Z" fill="white"/>
12+
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.3839 18.6161C45.872 19.1043 45.872 19.8957 45.3839 20.3839L37.8839 27.8839C37.3957 28.372 36.6043 28.372 36.1161 27.8839C35.628 27.3957 35.628 26.6043 36.1161 26.1161L43.6161 18.6161C44.1043 18.128 44.8957 18.128 45.3839 18.6161Z" fill="white"/>
13+
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.1161 18.6161C36.6043 18.128 37.3957 18.128 37.8839 18.6161L45.3839 26.1161C45.872 26.6043 45.872 27.3957 45.3839 27.8839C44.8957 28.372 44.1043 28.372 43.6161 27.8839L36.1161 20.3839C35.628 19.8957 35.628 19.1043 36.1161 18.6161Z" fill="white"/>
14+
</svg>
15+
`;

src/components/BaseFeedWidget.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const BaseFeedWidget = ({
5252
const widgetName = name ?? config?.name ?? url;
5353

5454
const onEdit = (): void => {
55-
rootNavigation.navigate('Widget', { url });
55+
rootNavigation.navigate('FeedWidget', { url });
5656
};
5757

5858
const onDelete = (): void => {
@@ -89,7 +89,6 @@ const BaseFeedWidget = ({
8989
<View style={styles.actions}>
9090
<TouchableOpacity
9191
style={styles.actionButton}
92-
activeOpacity={0.7}
9392
color="transparent"
9493
hitSlop={{ top: 15, bottom: 15 }}
9594
testID="WidgetActionDelete"
@@ -98,7 +97,6 @@ const BaseFeedWidget = ({
9897
</TouchableOpacity>
9998
<TouchableOpacity
10099
style={styles.actionButton}
101-
activeOpacity={0.7}
102100
color="transparent"
103101
hitSlop={{ top: 15, bottom: 15 }}
104102
testID="WidgetActionEdit"
@@ -107,7 +105,6 @@ const BaseFeedWidget = ({
107105
</TouchableOpacity>
108106
<TouchableOpacity
109107
style={styles.actionButton}
110-
activeOpacity={0.7}
111108
color="transparent"
112109
hitSlop={{ top: 15, bottom: 15 }}
113110
testID="WidgetActionDrag"

src/components/Widgets.tsx

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
widgetsSelector,
2222
} from '../store/reselect/widgets';
2323
import { setWidgetsSortOrder } from '../store/slices/widgets';
24-
import { TFeedWidget, TWidget } from '../store/types/widgets';
24+
import { TFeedWidget } from '../store/types/widgets';
2525
import { TouchableOpacity, View } from '../styles/components';
2626
import { Checkmark, PlusIcon, SortAscendingIcon } from '../styles/icons';
2727
import { Caption13Up } from '../styles/text';
@@ -33,10 +33,12 @@ import HeadlinesWidget from './HeadlinesWidget';
3333
import LuganoFeedWidget from './LuganoFeedWidget';
3434
import PriceWidget from './PriceWidget';
3535
import Button from './buttons/Button';
36+
import CalculatorWidget from './widgets/CalculatorWidget';
3637

3738
const Widgets = (): ReactElement => {
3839
const { t } = useTranslation('slashtags');
3940
const dispatch = useAppDispatch();
41+
4042
const widgets = useAppSelector(widgetsSelector);
4143
const sortOrder = useAppSelector(widgetsOrderSelector);
4244
const onboardedWidgets = useAppSelector(onboardedWidgetsSelector);
@@ -45,15 +47,20 @@ const Widgets = (): ReactElement => {
4547
useFocusEffect(useCallback(() => setEditing(false), []));
4648

4749
const sortedWidgets = useMemo(() => {
48-
const savedWidgets = Object.entries(widgets) as [string, TWidget][];
49-
return savedWidgets.sort(
50-
([a], [b]) => sortOrder.indexOf(a) - sortOrder.indexOf(b),
51-
);
50+
const savedWidgets = Object.keys(widgets);
51+
52+
const sorted = savedWidgets.sort((a, b) => {
53+
const indexA = sortOrder.indexOf(a);
54+
const indexB = sortOrder.indexOf(b);
55+
return indexA - indexB;
56+
});
57+
58+
return sorted;
5259
}, [widgets, sortOrder]);
5360

5461
const onDragEnd = useCallback(
5562
({ data }) => {
56-
const order = data.map((i): string => i[0]);
63+
const order = data.map((id): string => id);
5764
dispatch(setWidgetsSortOrder(order));
5865
},
5966
[dispatch],
@@ -67,25 +74,41 @@ const Widgets = (): ReactElement => {
6774
};
6875

6976
const renderItem = useCallback(
70-
({ item, drag }: RenderItemParams<[string, TWidget]>): ReactElement => {
71-
const [url, widget] = item;
72-
73-
const _drag = (): void => {
77+
({ item: id, drag }: RenderItemParams<string>): ReactElement => {
78+
const initiateDrag = (): void => {
7479
// only allow dragging if there are more than 1 widget
7580
if (sortedWidgets.length > 1 && editing) {
7681
drag();
7782
}
7883
};
7984

80-
const feedWidget = widget as TFeedWidget;
81-
8285
let testID: string;
8386
let Component:
8487
| typeof PriceWidget
8588
| typeof HeadlinesWidget
8689
| typeof BlocksWidget
8790
| typeof FactsWidget
88-
| typeof FeedWidget;
91+
| typeof FeedWidget
92+
| typeof CalculatorWidget;
93+
94+
if (id === 'calculator') {
95+
Component = CalculatorWidget;
96+
testID = 'CalculatorWidget';
97+
98+
return (
99+
<ScaleDecorator>
100+
<Component
101+
style={styles.widget}
102+
isEditing={editing}
103+
testID={testID}
104+
onLongPress={initiateDrag}
105+
onPressIn={initiateDrag}
106+
/>
107+
</ScaleDecorator>
108+
);
109+
}
110+
111+
const feedWidget = widgets[id] as TFeedWidget;
89112

90113
switch (feedWidget.type) {
91114
case SUPPORTED_FEED_TYPES.PRICE_FEED:
@@ -117,17 +140,17 @@ const Widgets = (): ReactElement => {
117140
<ScaleDecorator>
118141
<Component
119142
style={styles.widget}
120-
url={url}
143+
url={id}
121144
widget={feedWidget}
122145
isEditing={editing}
123-
onLongPress={_drag}
124-
onPressIn={_drag}
125146
testID={testID}
147+
onLongPress={initiateDrag}
148+
onPressIn={initiateDrag}
126149
/>
127150
</ScaleDecorator>
128151
);
129152
},
130-
[editing, sortedWidgets.length],
153+
[editing, widgets, sortedWidgets.length],
131154
);
132155

133156
return (
@@ -136,7 +159,6 @@ const Widgets = (): ReactElement => {
136159
<Caption13Up color="secondary">{t('widgets')}</Caption13Up>
137160
{sortedWidgets.length > 0 && (
138161
<TouchableOpacity
139-
activeOpacity={0.7}
140162
hitSlop={{ top: 15, right: 15, bottom: 15, left: 15 }}
141163
testID="WidgetsEdit"
142164
onPress={(): void => setEditing(!editing)}>
@@ -151,7 +173,7 @@ const Widgets = (): ReactElement => {
151173

152174
<DraggableFlatList
153175
data={sortedWidgets}
154-
keyExtractor={(item): string => item[0]}
176+
keyExtractor={(id): string => id}
155177
renderItem={renderItem}
156178
scrollEnabled={false}
157179
activationDistance={editing ? 0 : 100}

src/components/widgets/BaseWidget.tsx

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import React, { memo, ReactElement, useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
4+
5+
import { useNavigation } from '@react-navigation/native';
6+
import { widgets } from '../../constants/widgets';
7+
import { useAppDispatch, useAppSelector } from '../../hooks/redux';
8+
import { RootNavigationProp } from '../../navigation/types';
9+
import { showWidgetTitlesSelector } from '../../store/reselect/settings';
10+
import { deleteWidget } from '../../store/slices/widgets';
11+
import { TouchableOpacity } from '../../styles/components';
12+
import { ListIcon, SettingsIcon, TrashIcon } from '../../styles/icons';
13+
import { BodyMSB } from '../../styles/text';
14+
import { truncate } from '../../utils/helpers';
15+
import Dialog from '../Dialog';
16+
import SvgImage from '../SvgImage';
17+
18+
const BaseWidget = ({
19+
id,
20+
children,
21+
isEditing,
22+
style,
23+
testID,
24+
onPress,
25+
onPressIn,
26+
onLongPress,
27+
}: {
28+
id: string;
29+
children: ReactElement;
30+
isEditing?: boolean;
31+
style?: StyleProp<ViewStyle>;
32+
testID?: string;
33+
onPress?: () => void;
34+
onPressIn?: () => void;
35+
onLongPress?: () => void;
36+
}): ReactElement => {
37+
const { t } = useTranslation('widgets');
38+
const navigation = useNavigation<RootNavigationProp>();
39+
const dispatch = useAppDispatch();
40+
const [showDialog, setShowDialog] = useState(false);
41+
const showTitle = useAppSelector(showWidgetTitlesSelector);
42+
43+
const widget = {
44+
name: t(`${id}.name`),
45+
icon: widgets[id].icon,
46+
};
47+
48+
const onEdit = (): void => {
49+
navigation.navigate('Widget', { id });
50+
};
51+
52+
const onDelete = (): void => {
53+
setShowDialog(true);
54+
};
55+
56+
return (
57+
<>
58+
<TouchableOpacity
59+
style={[styles.root, style]}
60+
color="white10"
61+
activeOpacity={0.9}
62+
testID={testID}
63+
onPress={onPress}
64+
onPressIn={onPressIn}
65+
onLongPress={onLongPress}>
66+
{(showTitle || isEditing) && (
67+
<View style={styles.header}>
68+
<View style={styles.title}>
69+
<View style={styles.icon}>
70+
<SvgImage image={widget.icon} size={32} />
71+
</View>
72+
73+
<BodyMSB style={styles.name} numberOfLines={1}>
74+
{truncate(widget.name, 18)}
75+
</BodyMSB>
76+
</View>
77+
78+
{isEditing && (
79+
<View style={styles.actions}>
80+
<TouchableOpacity
81+
style={styles.actionButton}
82+
color="transparent"
83+
hitSlop={{ top: 15, bottom: 15 }}
84+
testID="WidgetActionDelete"
85+
onPress={onDelete}>
86+
<TrashIcon width={22} />
87+
</TouchableOpacity>
88+
<TouchableOpacity
89+
style={styles.actionButton}
90+
color="transparent"
91+
hitSlop={{ top: 15, bottom: 15 }}
92+
testID="WidgetActionEdit"
93+
onPress={onEdit}>
94+
<SettingsIcon width={22} />
95+
</TouchableOpacity>
96+
<TouchableOpacity
97+
style={styles.actionButton}
98+
color="transparent"
99+
hitSlop={{ top: 15, bottom: 15 }}
100+
testID="WidgetActionDrag"
101+
onLongPress={onLongPress}
102+
onPressIn={onPressIn}>
103+
<ListIcon color="white" width={24} />
104+
</TouchableOpacity>
105+
</View>
106+
)}
107+
</View>
108+
)}
109+
110+
{showTitle && !isEditing && <View style={styles.spacer} />}
111+
112+
{!isEditing && children}
113+
</TouchableOpacity>
114+
115+
<Dialog
116+
visible={showDialog}
117+
title={t('delete.title')}
118+
description={t('delete.description', { name: widget.name })}
119+
confirmText={t('delete_yes')}
120+
onCancel={(): void => {
121+
setShowDialog(false);
122+
}}
123+
onConfirm={(): void => {
124+
dispatch(deleteWidget(id));
125+
setShowDialog(false);
126+
}}
127+
/>
128+
</>
129+
);
130+
};
131+
132+
const styles = StyleSheet.create({
133+
root: {
134+
borderRadius: 16,
135+
padding: 16,
136+
},
137+
header: {
138+
flexDirection: 'row',
139+
alignItems: 'center',
140+
justifyContent: 'space-between',
141+
},
142+
title: {
143+
flexDirection: 'row',
144+
alignItems: 'center',
145+
},
146+
icon: {
147+
marginRight: 16,
148+
borderRadius: 6.4,
149+
overflow: 'hidden',
150+
height: 32,
151+
width: 32,
152+
},
153+
name: {
154+
lineHeight: 22,
155+
},
156+
actions: {
157+
flexDirection: 'row',
158+
justifyContent: 'center',
159+
alignItems: 'center',
160+
},
161+
actionButton: {
162+
alignItems: 'center',
163+
justifyContent: 'center',
164+
width: 32,
165+
height: 32,
166+
marginLeft: 8,
167+
},
168+
spacer: {
169+
height: 16,
170+
},
171+
content: {
172+
justifyContent: 'center',
173+
},
174+
});
175+
176+
export default memo(BaseWidget);

0 commit comments

Comments
 (0)