From 4d7284b150bd33d240eb56a0402a81bfd9667561 Mon Sep 17 00:00:00 2001 From: mauricio-degregori Date: Tue, 19 May 2026 21:51:09 -0500 Subject: [PATCH] Add trade sharing cards for positions and history --- src/components/layout/Modals.svelte | 7 +- src/components/modals/CustomizeColumns.svelte | 6 +- src/components/modals/ShareTrade.svelte | 149 +++++++++++++++++ src/components/trade/account/Account.svelte | 7 +- src/components/trade/account/History.svelte | 16 +- src/components/trade/account/Positions.svelte | 18 +- src/lib/icons.js | 7 +- src/lib/share.js | 156 ++++++++++++++++++ src/lib/stores.js | 2 +- 9 files changed, 355 insertions(+), 13 deletions(-) create mode 100644 src/components/modals/ShareTrade.svelte create mode 100644 src/lib/share.js diff --git a/src/components/layout/Modals.svelte b/src/components/layout/Modals.svelte index 03b829d..056a995 100644 --- a/src/components/layout/Modals.svelte +++ b/src/components/layout/Modals.svelte @@ -14,6 +14,7 @@ import UnstakeCAP from '../modals/UnstakeCAP.svelte' import HistoryOrderStatus from '../modals/HistoryOrderStatus.svelte' import Settings from '../modals/Settings.svelte' + import ShareTrade from '../modals/ShareTrade.svelte' @@ -67,4 +68,8 @@ {#if $activeModal && $activeModal.name == 'MarketInfo'} -{/if} \ No newline at end of file +{/if} + +{#if $activeModal && $activeModal.name == 'ShareTrade'} + +{/if} diff --git a/src/components/modals/CustomizeColumns.svelte b/src/components/modals/CustomizeColumns.svelte index c0dd5ef..1be8e0c 100644 --- a/src/components/modals/CustomizeColumns.svelte +++ b/src/components/modals/CustomizeColumns.svelte @@ -10,6 +10,10 @@ let isColumnShown = {}; + for (const col of data.allColumns) { + if (col.permanent) isColumnShown[col.key] = true; + } + let columnsShown; if (data.panel == 'orders') { for (const key of $ordersColumnsToShow) { @@ -31,7 +35,7 @@ // set store let columnsToShow = []; for (const col of data.allColumns) { - if (isColumnShown[col.key]) columnsToShow.push(col.key); + if (col.permanent || isColumnShown[col.key]) columnsToShow.push(col.key); } if (data.panel == 'orders') { ordersColumnsToShow.set(columnsToShow); diff --git a/src/components/modals/ShareTrade.svelte b/src/components/modals/ShareTrade.svelte new file mode 100644 index 0000000..e59fe76 --- /dev/null +++ b/src/components/modals/ShareTrade.svelte @@ -0,0 +1,149 @@ + + + + + +
+
+ {#if previewUrl} + Trade share card preview + {:else} +
Generating share card...
+ {/if} +
+ +
{shareText}
+ +
+
+
+
diff --git a/src/components/trade/account/Account.svelte b/src/components/trade/account/Account.svelte index a589a71..e63a878 100644 --- a/src/components/trade/account/Account.svelte +++ b/src/components/trade/account/Account.svelte @@ -45,7 +45,7 @@ {key: 'upl', gridTemplate: '1fr', sortable: true}, {key: 'funding', gridTemplate: '1fr', sortable: true}, {key: 'liqprice', gridTemplate: '1fr', sortable: true}, - {key: 'tools', gridTemplate: '30px', sortable: false, permanent: true} + {key: 'tools', gridTemplate: '72px', sortable: false, permanent: true} ], history: [ {key: 'id', gridTemplate: '0.4fr', sortable: true}, @@ -63,7 +63,8 @@ {key: 'pnl', gridTemplate: '1fr', sortable: true}, {key: 'fee', gridTemplate: '0.75fr', sortable: true}, {key: 'expiry', gridTemplate: '1fr', sortable: true}, - {key: 'cancelOrderId', gridTemplate: '0.5fr', sortable: false} + {key: 'cancelOrderId', gridTemplate: '0.5fr', sortable: false}, + {key: 'tools', gridTemplate: '30px', sortable: false, permanent: true} ] }; @@ -205,4 +206,4 @@ {#if panel == 'history'}{/if} - \ No newline at end of file + diff --git a/src/components/trade/account/History.svelte b/src/components/trade/account/History.svelte index 68ee72e..d0415e1 100644 --- a/src/components/trade/account/History.svelte +++ b/src/components/trade/account/History.svelte @@ -6,7 +6,8 @@ import Cell from '@components/layout/table/Cell.svelte' import { onMount, onDestroy } from 'svelte' - import { LOADING_ICON } from '@lib/icons' + import tooltip from '@lib/tooltip' + import { LOADING_ICON, SHARE_ICON } from '@lib/icons' import { DEFAULT_HISTORY_COUNT, DEFAULT_HISTORY_SORT_KEY } from '@lib/config' import { @@ -100,7 +101,7 @@ }); let columns = []; - $: columns = allColumns.filter((item) => $historyColumnsToShow.includes(item.key)); + $: columns = allColumns.filter((item) => item.permanent || $historyColumnsToShow.includes(item.key)); let formattedHistory = []; $: formattedHistory = $historySorted.map((item) => formatHistoryItem(item)); @@ -324,6 +325,15 @@ {item.cancelOrderId * 1 > 0 ? item.cancelOrderId : '-'} {/if} + + { + showModal('ShareTrade', { + kind: 'history', + trade: item + }) + }}>{@html SHARE_ICON} + + @@ -426,4 +436,4 @@ {#if loadingMore}
{@html LOADING_ICON}
{/if} - --> \ No newline at end of file + --> diff --git a/src/components/trade/account/Positions.svelte b/src/components/trade/account/Positions.svelte index bc4ccbe..5e0c34f 100644 --- a/src/components/trade/account/Positions.svelte +++ b/src/components/trade/account/Positions.svelte @@ -10,7 +10,7 @@ import { onDestroy } from 'svelte' - import { XMARK_ICON, PENCIL_ICON } from '@lib/icons' + import { XMARK_ICON, PENCIL_ICON, SHARE_ICON } from '@lib/icons' import { DEFAULT_POSITIONS_SORT_KEY, BPS_DIVIDER } from '@lib/config' import { @@ -42,7 +42,7 @@ $: fetchData($address); let columns = []; - $: columns = allColumns.filter((item) => $positionsColumnsToShow.includes(item.key)); + $: columns = allColumns.filter((item) => item.permanent || $positionsColumnsToShow.includes(item.key)); let formattedPositions = []; $: formattedPositions = $positionsSorted.map((pos) => formatPosition(pos)); @@ -334,6 +334,18 @@ {/if} + { + showModal('ShareTrade', { + kind: 'position', + trade: position, + meta: { + currentPrice: $prices[position.market], + funding: fundings[`${position.asset}:${position.market}`], + liqPrice: liqPrices[`${position.asset}:${position.market}`], + pnl: totalUpls[`${position.asset}:${position.market}`] + } + }) + }}>{@html SHARE_ICON} { showModal('EditMargin', {position, funding: fundings[`${position.asset}:${position.market}`]}) }}>{@html PENCIL_ICON} { showModal('ClosePosition', position) }}>{@html XMARK_ICON} @@ -415,4 +427,4 @@ {/each} -{/if} --> \ No newline at end of file +{/if} --> diff --git a/src/lib/icons.js b/src/lib/icons.js index 36c12fd..6012afc 100644 --- a/src/lib/icons.js +++ b/src/lib/icons.js @@ -183,6 +183,11 @@ export const WARNING_ICON = ` `; +export const SHARE_ICON = ` + + +`; + export const CHECKMARK_CIRCLE_ICON = ` @@ -191,4 +196,4 @@ export const CHECKMARK_CIRCLE_ICON = ` export const CHECKMARK_CIRCLE_INVERTED_ICON = ` -`; \ No newline at end of file +`; diff --git a/src/lib/share.js b/src/lib/share.js new file mode 100644 index 0000000..31f1398 --- /dev/null +++ b/src/lib/share.js @@ -0,0 +1,156 @@ +import { formatDate, formatForDisplay, formatMarketName, formatPnl, formatPriceForDisplay, formatSide } from './formatters' + +function stripHtml(value) { + return (value || '').replace(/<[^>]+>/g, '') +} + +function formatMarketLabel(market) { + return stripHtml(formatMarketName(market)) +} + +function formatPriceValue(value) { + if (value === undefined || value === null || value === '' || isNaN(value * 1)) return '-' + return formatPriceForDisplay(value) +} + +function formatAssetValue(value, asset) { + if (value === undefined || value === null || value === '' || isNaN(value * 1)) return '-' + return `${formatForDisplay(value)} ${asset}` +} + +export function buildTradeShareData(kind, trade, meta = {}) { + const isHistory = kind == 'history' + const side = isHistory ? formatSide(trade.isLong, trade.isReduceOnly, trade.pnl) : formatSide(trade.isLong) + const pnl = meta.pnl ?? trade.pnl + const status = isHistory ? (trade.status || '-') : 'Open' + const timestamp = formatDate(trade.timestamp) + + const fields = [ + { label: 'Entry Price', value: formatPriceValue(trade.price) }, + { label: 'Size', value: formatAssetValue(trade.size, trade.asset) }, + { label: 'Margin', value: formatAssetValue(trade.margin, trade.asset) }, + { label: 'Leverage', value: trade.leverage ? `${formatForDisplay(trade.leverage)}×` : '-' } + ] + + if (isHistory) { + fields.push( + { label: 'Order Status', value: status }, + { label: 'P/L', value: pnl !== undefined ? `${formatPnl(pnl)} ${trade.asset}` : '-' }, + { label: 'Fee', value: formatAssetValue(trade.fee, trade.asset) }, + { label: 'Submitted', value: timestamp || '-' } + ) + } else { + fields.push( + { label: 'Current Price', value: formatPriceValue(meta.currentPrice) }, + { label: 'P/L', value: pnl !== undefined ? `${formatPnl(pnl)} ${trade.asset}` : '-' }, + { label: 'Funding', value: formatAssetValue(meta.funding, trade.asset) }, + { label: 'Liq. Price', value: formatPriceValue(meta.liqPrice) } + ) + } + + return { + kind, + title: isHistory ? 'Trade Recap' : 'Open Position', + market: formatMarketLabel(trade.market), + side, + status, + timestamp, + pnl, + asset: trade.asset, + fileName: `cap-${kind}-${String(trade.market || 'trade').toLowerCase().replace(/[^a-z0-9]+/g, '-')}.png`, + fields + } +} + +export function buildTradeShareText(shareData) { + return [ + `CAP ${shareData.title}`, + `${shareData.market} • ${shareData.side}`, + ...shareData.fields.map((field) => `${field.label}: ${field.value}`) + ].join('\n') +} + +export async function renderTradeShareCard(shareData) { + const width = 1200 + const height = 675 + const canvas = document.createElement('canvas') + canvas.width = width + canvas.height = height + const ctx = canvas.getContext('2d') + + const gradient = ctx.createLinearGradient(0, 0, width, height) + gradient.addColorStop(0, '#0f172a') + gradient.addColorStop(0.55, '#111827') + gradient.addColorStop(1, '#020617') + ctx.fillStyle = gradient + ctx.fillRect(0, 0, width, height) + + ctx.fillStyle = 'rgba(255,255,255,0.05)' + ctx.fillRect(54, 54, width - 108, height - 108) + ctx.strokeStyle = 'rgba(255,255,255,0.08)' + ctx.lineWidth = 2 + ctx.strokeRect(54, 54, width - 108, height - 108) + + const accent = shareData.side.toLowerCase().includes('short') ? '#ff7a7a' : '#72f2a4' + ctx.fillStyle = accent + ctx.fillRect(86, 94, 14, 108) + + ctx.fillStyle = '#f8fafc' + ctx.font = '700 28px sans-serif' + ctx.fillText('CAP', 126, 120) + ctx.font = '700 54px sans-serif' + ctx.fillText(shareData.market, 126, 184) + + ctx.font = '600 28px sans-serif' + ctx.fillStyle = accent + ctx.fillText(shareData.side, 126, 228) + + ctx.textAlign = 'right' + ctx.fillStyle = '#cbd5e1' + ctx.font = '600 26px sans-serif' + ctx.fillText(shareData.title, width - 96, 120) + ctx.font = '500 22px sans-serif' + ctx.fillText(shareData.timestamp || '', width - 96, 160) + ctx.textAlign = 'left' + + const startY = 298 + const boxWidth = 480 + const boxHeight = 88 + const gapX = 44 + const gapY = 24 + + shareData.fields.forEach((field, index) => { + const col = index % 2 + const row = Math.floor(index / 2) + const x = 86 + col * (boxWidth + gapX) + const y = startY + row * (boxHeight + gapY) + + ctx.fillStyle = 'rgba(15, 23, 42, 0.74)' + ctx.fillRect(x, y, boxWidth, boxHeight) + ctx.strokeStyle = 'rgba(148, 163, 184, 0.18)' + ctx.lineWidth = 1 + ctx.strokeRect(x, y, boxWidth, boxHeight) + + ctx.fillStyle = '#94a3b8' + ctx.font = '600 18px sans-serif' + ctx.fillText(field.label.toUpperCase(), x + 24, y + 28) + + ctx.fillStyle = field.label == 'P/L' && shareData.pnl !== undefined ? (shareData.pnl * 1 >= 0 ? '#72f2a4' : '#ff7a7a') : '#f8fafc' + ctx.font = '700 28px sans-serif' + ctx.fillText(field.value, x + 24, y + 62) + }) + + ctx.fillStyle = 'rgba(148, 163, 184, 0.9)' + ctx.font = '500 20px sans-serif' + ctx.fillText('Shareable trade snapshot generated on CAP.', 86, height - 88) + + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob) + } else { + reject(new Error('Failed to generate share image')) + } + }, 'image/png') + }) +} diff --git a/src/lib/stores.js b/src/lib/stores.js index d5761f7..9211680 100644 --- a/src/lib/stores.js +++ b/src/lib/stores.js @@ -159,7 +159,7 @@ export const positionsSorted = derived([positions, positionsSortKey], ([$positio // History export const history = writable([]); -export const historyColumnsToShow = writable(getUserSetting('historyColumnsToShow') || ['isLong', 'market', 'price', 'size', 'status', 'reason', 'pnl']); +export const historyColumnsToShow = writable(getUserSetting('historyColumnsToShow') || ['isLong', 'market', 'price', 'size', 'status', 'reason', 'pnl', 'tools']); export const historySortKey = writable(['timestamp', true]); // [columnName, isDesc] export const historySorted = derived([history, historySortKey], ([$history, $historySortKey]) => { return sorter($history, $historySortKey);