Skip to content

Commit

Permalink
feat(widgets): add calculator widget
Browse files Browse the repository at this point in the history
  • Loading branch information
pwltr committed Jan 15, 2025
1 parent ad01698 commit ebd3065
Show file tree
Hide file tree
Showing 29 changed files with 1,078 additions and 371 deletions.
6 changes: 6 additions & 0 deletions .tx/config
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,9 @@ file_filter = src/utils/i18n/locales/<lang>/wallet.json
source_file = src/utils/i18n/locales/en/wallet.json
source_lang = en
type = STRUCTURED_JSON

[o:synonym:p:bitkit:r:widgets]
file_filter = src/utils/i18n/locales/<lang>/widgets.json
source_file = src/utils/i18n/locales/en/widgets.json
source_lang = en
type = STRUCTURED_JSON
13 changes: 13 additions & 0 deletions src/assets/icons/widgets.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
export const bitfinexIcon = (): string =>
'<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>';

export const calculatorIcon = (): string =>
`<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="8" fill="#FF4400"/>
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
</svg>
`;
5 changes: 1 addition & 4 deletions src/components/BaseFeedWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const BaseFeedWidget = ({
const widgetName = name ?? config?.name ?? url;

const onEdit = (): void => {
rootNavigation.navigate('Widget', { url });
rootNavigation.navigate('FeedWidget', { url });
};

const onDelete = (): void => {
Expand Down Expand Up @@ -89,7 +89,6 @@ const BaseFeedWidget = ({
<View style={styles.actions}>
<TouchableOpacity
style={styles.actionButton}
activeOpacity={0.7}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDelete"
Expand All @@ -98,7 +97,6 @@ const BaseFeedWidget = ({
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
activeOpacity={0.7}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionEdit"
Expand All @@ -107,7 +105,6 @@ const BaseFeedWidget = ({
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
activeOpacity={0.7}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDrag"
Expand Down
60 changes: 41 additions & 19 deletions src/components/Widgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
widgetsSelector,
} from '../store/reselect/widgets';
import { setWidgetsSortOrder } from '../store/slices/widgets';
import { TFeedWidget, TWidget } from '../store/types/widgets';
import { TFeedWidget } from '../store/types/widgets';
import { TouchableOpacity, View } from '../styles/components';
import { Checkmark, PlusIcon, SortAscendingIcon } from '../styles/icons';
import { Caption13Up } from '../styles/text';
Expand All @@ -33,10 +33,12 @@ import HeadlinesWidget from './HeadlinesWidget';
import LuganoFeedWidget from './LuganoFeedWidget';
import PriceWidget from './PriceWidget';
import Button from './buttons/Button';
import CalculatorWidget from './widgets/CalculatorWidget';

const Widgets = (): ReactElement => {
const { t } = useTranslation('slashtags');
const dispatch = useAppDispatch();

const widgets = useAppSelector(widgetsSelector);
const sortOrder = useAppSelector(widgetsOrderSelector);
const onboardedWidgets = useAppSelector(onboardedWidgetsSelector);
Expand All @@ -45,15 +47,20 @@ const Widgets = (): ReactElement => {
useFocusEffect(useCallback(() => setEditing(false), []));

const sortedWidgets = useMemo(() => {
const savedWidgets = Object.entries(widgets) as [string, TWidget][];
return savedWidgets.sort(
([a], [b]) => sortOrder.indexOf(a) - sortOrder.indexOf(b),
);
const savedWidgets = Object.keys(widgets);

const sorted = savedWidgets.sort((a, b) => {
const indexA = sortOrder.indexOf(a);
const indexB = sortOrder.indexOf(b);
return indexA - indexB;
});

return sorted;
}, [widgets, sortOrder]);

const onDragEnd = useCallback(
({ data }) => {
const order = data.map((i): string => i[0]);
const order = data.map((id): string => id);
dispatch(setWidgetsSortOrder(order));
},
[dispatch],
Expand All @@ -67,25 +74,41 @@ const Widgets = (): ReactElement => {
};

const renderItem = useCallback(
({ item, drag }: RenderItemParams<[string, TWidget]>): ReactElement => {
const [url, widget] = item;

const _drag = (): void => {
({ item: id, drag }: RenderItemParams<string>): ReactElement => {
const initiateDrag = (): void => {
// only allow dragging if there are more than 1 widget
if (sortedWidgets.length > 1 && editing) {
drag();
}
};

const feedWidget = widget as TFeedWidget;

let testID: string;
let Component:
| typeof PriceWidget
| typeof HeadlinesWidget
| typeof BlocksWidget
| typeof FactsWidget
| typeof FeedWidget;
| typeof FeedWidget
| typeof CalculatorWidget;

if (id === 'calculator') {
Component = CalculatorWidget;
testID = 'CalculatorWidget';

return (
<ScaleDecorator>
<Component
style={styles.widget}
isEditing={editing}
testID={testID}
onLongPress={initiateDrag}
onPressIn={initiateDrag}
/>
</ScaleDecorator>
);
}

const feedWidget = widgets[id] as TFeedWidget;

switch (feedWidget.type) {
case SUPPORTED_FEED_TYPES.PRICE_FEED:
Expand Down Expand Up @@ -117,17 +140,17 @@ const Widgets = (): ReactElement => {
<ScaleDecorator>
<Component
style={styles.widget}
url={url}
url={id}
widget={feedWidget}
isEditing={editing}
onLongPress={_drag}
onPressIn={_drag}
testID={testID}
onLongPress={initiateDrag}
onPressIn={initiateDrag}
/>
</ScaleDecorator>
);
},
[editing, sortedWidgets.length],
[editing, widgets, sortedWidgets.length],
);

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

<DraggableFlatList
data={sortedWidgets}
keyExtractor={(item): string => item[0]}
keyExtractor={(id): string => id}
renderItem={renderItem}
scrollEnabled={false}
activationDistance={editing ? 0 : 100}
Expand Down
176 changes: 176 additions & 0 deletions src/components/widgets/BaseWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { memo, ReactElement, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';

import { useNavigation } from '@react-navigation/native';
import { widgets } from '../../constants/widgets';
import { useAppDispatch, useAppSelector } from '../../hooks/redux';
import { RootNavigationProp } from '../../navigation/types';
import { showWidgetTitlesSelector } from '../../store/reselect/settings';
import { deleteWidget } from '../../store/slices/widgets';
import { TouchableOpacity } from '../../styles/components';
import { ListIcon, SettingsIcon, TrashIcon } from '../../styles/icons';
import { BodyMSB } from '../../styles/text';
import { truncate } from '../../utils/helpers';
import Dialog from '../Dialog';
import SvgImage from '../SvgImage';

const BaseWidget = ({
id,
children,
isEditing,
style,
testID,
onPress,
onPressIn,
onLongPress,
}: {
id: string;
children: ReactElement;
isEditing?: boolean;
style?: StyleProp<ViewStyle>;
testID?: string;
onPress?: () => void;
onPressIn?: () => void;
onLongPress?: () => void;
}): ReactElement => {
const { t } = useTranslation('widgets');
const navigation = useNavigation<RootNavigationProp>();
const dispatch = useAppDispatch();
const [showDialog, setShowDialog] = useState(false);
const showTitle = useAppSelector(showWidgetTitlesSelector);

const widget = {
name: t(`${id}.name`),
icon: widgets[id].icon,
};

const onEdit = (): void => {
navigation.navigate('Widget', { id });
};

const onDelete = (): void => {
setShowDialog(true);
};

return (
<>
<TouchableOpacity
style={[styles.root, style]}
color="white10"
activeOpacity={0.9}
testID={testID}
onPress={onPress}
onPressIn={onPressIn}
onLongPress={onLongPress}>
{(showTitle || isEditing) && (
<View style={styles.header}>
<View style={styles.title}>
<View style={styles.icon}>
<SvgImage image={widget.icon} size={32} />
</View>

<BodyMSB style={styles.name} numberOfLines={1}>
{truncate(widget.name, 18)}
</BodyMSB>
</View>

{isEditing && (
<View style={styles.actions}>
<TouchableOpacity
style={styles.actionButton}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDelete"
onPress={onDelete}>
<TrashIcon width={22} />
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionEdit"
onPress={onEdit}>
<SettingsIcon width={22} />
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDrag"
onLongPress={onLongPress}
onPressIn={onPressIn}>
<ListIcon color="white" width={24} />
</TouchableOpacity>
</View>
)}
</View>
)}

{showTitle && !isEditing && <View style={styles.spacer} />}

{!isEditing && children}
</TouchableOpacity>

<Dialog
visible={showDialog}
title={t('delete.title')}
description={t('delete.description', { name: widget.name })}
confirmText={t('delete_yes')}
onCancel={(): void => {
setShowDialog(false);
}}
onConfirm={(): void => {
dispatch(deleteWidget(id));
setShowDialog(false);
}}
/>
</>
);
};

const styles = StyleSheet.create({
root: {
borderRadius: 16,
padding: 16,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginRight: 16,
borderRadius: 6.4,
overflow: 'hidden',
height: 32,
width: 32,
},
name: {
lineHeight: 22,
},
actions: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
actionButton: {
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: 32,
marginLeft: 8,
},
spacer: {
height: 16,
},
content: {
justifyContent: 'center',
},
});

export default memo(BaseWidget);
Loading

0 comments on commit ebd3065

Please sign in to comment.