Skip to content

Commit

Permalink
Maryia/positions-redesign/finilise contract card + add total profit l…
Browse files Browse the repository at this point in the history
…oss + initiate pagination in closed positions (#70)

* refactor: utilize Tag in ContractCardStatusTimer

* chore: add opacity transition to buttons when revealing/hiding them

* feat: add total profit + improve card

* fix: card deletion transition + total pnl positioning

* feat: add pagination on scroll (initial version)
  • Loading branch information
maryia-deriv authored May 27, 2024
1 parent 06c9356 commit c56fb28
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { formatDuration, getDiffDuration } from '@deriv/shared';
import { TGetCardLables } from '../types';

type TRemainingTimeProps = {
as?: React.ElementType;
end_time?: number;
start_time: moment.Moment;
format?: string;
getCardLabels: TGetCardLables;
};

const RemainingTime = ({ end_time, format, getCardLabels, start_time }: TRemainingTimeProps) => {
const RemainingTime = ({ as = 'div', end_time, format, getCardLabels, start_time }: TRemainingTimeProps) => {
const Tag = as;
if (!end_time || start_time.unix() > +end_time) {
return <React.Fragment>{''}</React.Fragment>;
}
Expand All @@ -24,7 +26,7 @@ const RemainingTime = ({ end_time, format, getCardLabels, start_time }: TRemaini
}
const is_zeroes = /^00:00$/.test(remaining_time);

return <React.Fragment>{!is_zeroes && <div className='dc-remaining-time'>{remaining_time}</div>}</React.Fragment>;
return <React.Fragment>{!is_zeroes && <Tag className='dc-remaining-time'>{remaining_time}</Tag>}</React.Fragment>;
};

export default RemainingTime;
3 changes: 1 addition & 2 deletions packages/reports/src/Containers/open-positions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -655,8 +655,7 @@ const OpenPositions = observer(({ component_icon, ...props }: TOpenPositions) =>
/>
);
};
// TODO: Uncomment and update this when DTrader 2.0 development starts:
// if (useFeatureFlags().is_dtrader_v2_enabled) return <Text size='l'>I am Open positions for DTrader 2.0.</Text>;

return (
<React.Fragment>
<NotificationMessages />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const ContractCard = 'Contract Card';
jest.mock('../contract-card', () => jest.fn(() => <div>{ContractCard}</div>));

const mockProps = {
onScroll: jest.fn(),
positions: [
{
contract_info: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { TPortfolioPosition } from '@deriv/stores/types';
import { Localize } from '@deriv/translations';
import { CaptionText } from '@deriv-com/quill-ui';
import { Tag } from '@deriv-com/quill-ui';
import { LabelPairedStopwatchCaptionRegularIcon } from '@deriv/quill-icons';
import { getCardLabels } from '@deriv/shared';
import { RemainingTime } from '@deriv/components';
Expand Down Expand Up @@ -29,20 +29,24 @@ export const ContractCardStatusTimer = ({
}
return (
<RemainingTime
as='span'
end_time={date_expiry}
getCardLabels={getCardLabels}
start_time={serverTime as moment.Moment}
/>
);
};
if (!date_expiry || (serverTime as moment.Moment).unix() > +date_expiry || isSold) {
return <CaptionText className='status'>{getCardLabels().CLOSED}</CaptionText>;
return <Tag className='status' label={getCardLabels().CLOSED} variant='custom' color='custom' size='sm' />;
}
return (
// TODO: when <Tag /> is exported from quill-ui, use it instead
<div className='timer'>
<LabelPairedStopwatchCaptionRegularIcon />
<CaptionText as='div'>{getDisplayedDuration()}</CaptionText>
</div>
<Tag
className='timer'
icon={LabelPairedStopwatchCaptionRegularIcon}
label={getDisplayedDuration()}
variant='custom'
color='custom'
size='sm'
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// TODO: Utilize tokens from quill and logical css properties;
.contract-card {
position: relative;
display: flex;
Expand All @@ -11,41 +10,33 @@
transform: translateX(0);
transition: transform var(--core-motion-duration-200) var(--motion-easing-inandout);

.icon,
.dc-icon {
position: relative;
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
width: var(--core-size-1600);
height: var(--core-size-1600);
cursor: pointer;
}
.body {
&__body {
display: flex;
flex-direction: column;
gap: var(--semantic-spacing-gap-md);
}
.body,
.title {
&__body,
&__title {
min-width: 0;
flex-grow: 1;
}
.details,
.status-and-profit,
.status,
.timer {
&__details,
.status-and-profit {
display: flex;
gap: var(--core-spacing-400);
align-items: center;
}
.status,
.timer {
background-color: var(--core-color-opacity-black-75);
height: var(--core-size-1200);
padding: 0 var(--core-spacing-400);
border-radius: var(--semantic-borderRadius-sm);

.dc-remaining-time {
font-size: unset;
Expand All @@ -54,7 +45,7 @@
.status-and-profit {
justify-content: space-between;
}
.title {
&__title {
display: flex;
flex-direction: column;
}
Expand All @@ -64,7 +55,7 @@
overflow: hidden;
text-overflow: ellipsis;

&__icon {
&-icon {
padding: var(--core-size-200);
}
}
Expand All @@ -87,18 +78,24 @@
inset-inline-end: 0;
inset-block-end: 0;
transform: translateX(100%);
opacity: var(--core-opacity-50);
transition: opacity var(--core-motion-duration-200) var(--motion-easing-inandout);

button {
display: flex;
justify-content: center;
align-items: center;
width: var(--core-size-3600);
height: 100%;
cursor: pointer;

div {
.label {
color: var(--core-color-solid-slate-50);
}
&:disabled:not(.loading) {
background-color: var(--core-color-opacity-black-200);

div {
.label {
color: var(--component-textIcon-normal-disabled);
}
}
Expand Down Expand Up @@ -136,7 +133,7 @@
}
}
&.lost {
.total-profit {
.profit {
color: var(--core-color-solid-red-600);
}
button:not(:disabled),
Expand All @@ -149,7 +146,7 @@
}
}
&.won {
.total-profit {
.profit {
color: var(--core-color-solid-green-600);
}
button:not(:disabled),
Expand All @@ -172,14 +169,15 @@
position: relative;
width: inherit;
overflow: hidden;
min-height: 10.4rem; // equal to __item's height of 104px, must be an exact value for item deletion transition to work
flex-shrink: 0;
max-height: 10.4rem; // Update carefully: max-height in exact units is needed for transition below to work
box-shadow: var(--core-elevation-shadow-130);
border-radius: var(--semantic-borderRadius-md);

&.deleted {
opacity: var(--core-opacity-50);
max-height: 0;
transition: max-height 0.3s, opacity 0.3s;
transition: max-height 0.3s, opacity 0.1s;
transition-timing-function: var(--motion-easing-out);
}
}
Expand Down Expand Up @@ -224,6 +222,12 @@
}
}
}
&.show-buttons,
&-list--has-buttons-demo {
.buttons {
opacity: var(--core-opacity-1300);
}
}
}

.contract-cards {
Expand Down
66 changes: 38 additions & 28 deletions packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
getCardLabels,
getCurrentTick,
getMarketName,
getTotalProfit,
getTradeTypeName,
isEnded,
isHighLow,
Expand All @@ -20,6 +19,7 @@ import { ContractCardStatusTimer, TContractCardStatusTimerProps } from './contra
import { BinaryLink } from 'App/Components/Routes';
import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content';
import { TRootStore } from 'Types';
import { getProfit } from 'AppV2/Utils/positions-utils';

type TContractCardProps = TContractCardStatusTimerProps & {
className?: string;
Expand All @@ -45,7 +45,7 @@ const swipeConfig = {
};

const ContractCard = ({
className,
className = 'contract-card',
contractInfo,
currency,
hasActionButtons,
Expand All @@ -57,6 +57,8 @@ const ContractCard = ({
serverTime,
}: TContractCardProps) => {
const [isDeleted, setIsDeleted] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const [isCanceling, setIsCanceling] = React.useState(false);
const [shouldShowButtons, setShouldShowButtons] = React.useState(false);
const { buy_price, contract_type, display_name, sell_time, shortcode } = contractInfo;
const contract_main_title = getTradeTypeName(contract_type ?? '', {
Expand All @@ -70,12 +72,8 @@ const ContractCard = ({
const symbolName =
'underlying_symbol' in contractInfo ? getMarketName(contractInfo.underlying_symbol ?? '') : display_name;
const isMultiplier = isMultiplierContract(contract_type);
const isSold = isEnded(contractInfo as TContractInfo);
const totalProfit =
(contractInfo as TClosedPosition['contract_info']).profit_loss ??
(isMultiplierContract(contract_type)
? getTotalProfit(contractInfo as TContractInfo)
: (contractInfo as TContractInfo).profit);
const isSold = !!sell_time || isEnded(contractInfo as TContractInfo);
const totalProfit = getProfit(contractInfo);
const validToCancel = isValidToCancel(contractInfo as TContractInfo);
const validToSell = isValidToSell(contractInfo as TContractInfo) && !isSellRequested;

Expand All @@ -93,7 +91,13 @@ const ContractCard = ({
const handleClose = (e: React.MouseEvent<HTMLButtonElement>, shouldCancel?: boolean) => {
e.preventDefault();
e.stopPropagation();
shouldCancel ? onCancel?.(e) : onClose?.(e);
if (shouldCancel) {
onCancel?.(e);
setIsCanceling(true);
} else {
onClose?.(e);
setIsClosing(true);
}
};

React.useEffect(() => {
Expand All @@ -104,10 +108,10 @@ const ContractCard = ({

if (!contract_type) return null;
return (
<div className={classNames('contract-card-wrapper', { deleted: isDeleted })}>
<div className={classNames(`${className}-wrapper`, { deleted: isDeleted })}>
<BinaryLink
{...(hasActionButtons ? swipeHandlers : {})}
className={classNames('contract-card', className, {
className={classNames(className, {
'show-buttons': shouldShowButtons,
'has-cancel-button': validToCancel,
lost: Number(totalProfit) < 0,
Expand All @@ -117,10 +121,10 @@ const ContractCard = ({
onDragStart={e => e.preventDefault()}
to={redirectTo}
>
<div className='body'>
<div className='details'>
<IconTradeTypes className='trade-type__icon' type={contract_type ?? ''} size={32} />
<div className='title'>
<div className={`${className}__body`}>
<div className={`${className}__details`}>
<IconTradeTypes className='trade-type-icon' type={contract_type ?? ''} size={32} />
<div className={`${className}__title`}>
<Text className='trade-type' size='sm'>
{tradeTypeName}
</Text>
Expand All @@ -136,11 +140,11 @@ const ContractCard = ({
<ContractCardStatusTimer
currentTick={currentTick}
hasNoAutoExpiry={isMultiplier}
isSold={!!sell_time || isSold}
isSold={isSold}
serverTime={serverTime}
{...contractInfo}
/>
<Text className='total-profit' size='sm'>
<Text className='profit' size='sm'>
<Money amount={totalProfit} currency={currency} has_sign show_currency />
</Text>
</div>
Expand All @@ -149,25 +153,31 @@ const ContractCard = ({
<div className='buttons'>
{validToCancel && (
<button
className={classNames('icon', 'cancel', { loading: isSellRequested })}
aria-label='cancel'
disabled={Number(totalProfit) >= 0}
className={classNames({ loading: isSellRequested && isCanceling })}
disabled={Number((contractInfo as TContractInfo).profit) >= 0}
onClick={e => handleClose(e, true)}
>
<CaptionText bold as='div'>
{isSellRequested ? <div className='circle-loader' /> : getCardLabels().CANCEL}
</CaptionText>
{isSellRequested && isCanceling ? (
<div className='circle-loader' />
) : (
<CaptionText bold as='div' className='label'>
{getCardLabels().CANCEL}
</CaptionText>
)}
</button>
)}
<button
className={classNames('icon', 'close', { loading: isSellRequested })}
aria-label='close'
className={classNames({ loading: isSellRequested && isClosing })}
disabled={!validToSell}
onClick={handleClose}
>
<CaptionText bold as='div'>
{isSellRequested ? <div className='circle-loader' /> : getCardLabels().CLOSE}
</CaptionText>
{isSellRequested && isClosing ? (
<div className='circle-loader' />
) : (
<CaptionText bold as='div' className='label'>
{getCardLabels().CLOSE}
</CaptionText>
)}
</button>
</div>
)}
Expand Down
Loading

0 comments on commit c56fb28

Please sign in to comment.