diff --git a/apps/oeth/index.html b/apps/oeth/index.html index d55247934..aaf7aa420 100644 --- a/apps/oeth/index.html +++ b/apps/oeth/index.html @@ -1,4 +1,4 @@ - + @@ -7,7 +7,14 @@ - + + + + +
diff --git a/apps/oeth/src/components/App.tsx b/apps/oeth/src/components/App.tsx new file mode 100644 index 000000000..45621181b --- /dev/null +++ b/apps/oeth/src/components/App.tsx @@ -0,0 +1,17 @@ +import { HistoryView, SwapView, WrapView } from '@origin/defi/oeth'; +import { Route, Routes } from 'react-router-dom'; + +import { Layout } from './Layout'; + +export function App() { + return ( + + }> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/apps/oeth/src/components/Layout.tsx b/apps/oeth/src/components/Layout.tsx new file mode 100644 index 000000000..b404853c0 --- /dev/null +++ b/apps/oeth/src/components/Layout.tsx @@ -0,0 +1,39 @@ +import { Container } from '@mui/material'; +import { TopNav } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; +import { Outlet } from 'react-router-dom'; + +export function Layout() { + const intl = useIntl(); + + return ( + <> + + + + + + ); +} diff --git a/apps/oeth/src/components/index.tsx b/apps/oeth/src/components/index.tsx new file mode 100644 index 000000000..9342f378e --- /dev/null +++ b/apps/oeth/src/components/index.tsx @@ -0,0 +1 @@ +export * from './App'; \ No newline at end of file diff --git a/apps/oeth/src/index.tsx b/apps/oeth/src/index.tsx deleted file mode 100644 index 0fb71c82c..000000000 --- a/apps/oeth/src/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// placeholder component until we actually start migration -export function OUSDRoot() { - return

ousd

-} \ No newline at end of file diff --git a/apps/oeth/src/main.tsx b/apps/oeth/src/main.tsx index 1898e9b7f..5e65c7a48 100644 --- a/apps/oeth/src/main.tsx +++ b/apps/oeth/src/main.tsx @@ -1,7 +1,6 @@ import { StrictMode } from 'react'; import * as ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; -import { OethRoot } from './views/root'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from '@origin/shared/data-access'; import { theme } from '@origin/shared/theme'; @@ -11,6 +10,7 @@ import { } from '@mui/material'; import { IntlProvider } from 'react-intl'; import { en } from './lang'; +import { App } from './components'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement @@ -22,7 +22,7 @@ root.render( - + diff --git a/apps/oeth/src/views/root/index.tsx b/apps/oeth/src/views/root/index.tsx deleted file mode 100644 index 517bdaba2..000000000 --- a/apps/oeth/src/views/root/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// placeholder component until we actually start migration -import { DefiOeth } from '@origin/defi/oeth'; -import { useIntl } from 'react-intl'; - -export function OethRoot() { - const intl = useIntl(); - return ( - <> -

{intl.formatMessage({ defaultMessage: 'test OEth' })}

- - - ); -} diff --git a/apps/oeth/tsconfig.app.json b/apps/oeth/tsconfig.app.json index e4322b551..eb2d36377 100644 --- a/apps/oeth/tsconfig.app.json +++ b/apps/oeth/tsconfig.app.json @@ -18,5 +18,5 @@ "src/**/*.spec.jsx", "src/**/*.test.jsx" ], - "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", "../../libs/defi/oeth/src/components/TopNav.tsx"] } diff --git a/apps/oeth/vite.config.ts b/apps/oeth/vite.config.ts index 39f6c9cba..89ae276f5 100644 --- a/apps/oeth/vite.config.ts +++ b/apps/oeth/vite.config.ts @@ -1,6 +1,10 @@ /// -import { defineConfig } from 'vite'; +/// import react from '@vitejs/plugin-react'; +import path from 'path'; +import { defineConfig } from 'vite'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; +import svgr from 'vite-plugin-svgr'; import viteTsConfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ @@ -17,6 +21,7 @@ export default defineConfig({ }, plugins: [ + svgr(), react({ babel: { plugins: [ @@ -33,6 +38,14 @@ export default defineConfig({ viteTsConfigPaths({ root: '../../', }), + viteStaticCopy({ + targets: [ + { + src: path.resolve(__dirname, '../../libs/shared/assets/files/*'), + dest: './images', + }, + ], + }), ], // Uncomment this if you are using workers. diff --git a/apps/ousd/index.html b/apps/ousd/index.html index 53b2040b3..01bc9d957 100644 --- a/apps/ousd/index.html +++ b/apps/ousd/index.html @@ -1,4 +1,4 @@ - + @@ -7,7 +7,6 @@ -
diff --git a/libs/defi/oeth/src/components/History/HistoryButton.stories.tsx b/libs/defi/oeth/src/components/History/HistoryButton.stories.tsx new file mode 100644 index 000000000..6943e2112 --- /dev/null +++ b/libs/defi/oeth/src/components/History/HistoryButton.stories.tsx @@ -0,0 +1,30 @@ +import { HistoryFilterButton } from './HistoryButton'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: HistoryFilterButton, + title: 'History/History filter button', + args: { + circle: false, + children: 'Test', + }, + render: (args) => , +}; + +export default meta; + +export const Primary: StoryObj = {}; + +export const WithCircle: StoryObj = { + args: { + circle: true, + }, +}; + +export const Selected: StoryObj = { + args: { + circle: true, + selected: true, + }, +}; diff --git a/libs/defi/oeth/src/components/History/HistoryButton.tsx b/libs/defi/oeth/src/components/History/HistoryButton.tsx new file mode 100644 index 000000000..9c5f4b672 --- /dev/null +++ b/libs/defi/oeth/src/components/History/HistoryButton.tsx @@ -0,0 +1,69 @@ +import { alpha, Box, Button } from '@mui/material'; + +import type { ButtonProps, SxProps } from '@mui/material'; +import type { Theme } from '@origin/shared/theme'; + +interface Props extends ButtonProps { + circle?: boolean; + selected?: boolean; +} + +export function HistoryFilterButton({ + children, + circle = false, + onClick, + selected = false, + sx, + ...rest +}: Props) { + return ( + + ); +} + +function Circle({ sx }: { sx: SxProps }) { + return ( + theme.palette.background.default, + height: '0.5rem', + width: '0.5rem', + borderRadius: '100%', + ...sx, + }} + /> + ); +} diff --git a/libs/defi/oeth/src/components/History/HistoryCard.tsx b/libs/defi/oeth/src/components/History/HistoryCard.tsx new file mode 100644 index 000000000..c13d944c7 --- /dev/null +++ b/libs/defi/oeth/src/components/History/HistoryCard.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; + +import { Box, Button, Stack, Typography } from '@mui/material'; +import { Card } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; + +import { HistoryFilterButton } from './HistoryButton'; +import { HistoryTable } from './HistoryTable'; + +import type { ColumnFilter } from '@tanstack/react-table'; + +export function HistoryCard() { + const [isConnected, setConnectionState] = useState(false); + const intl = useIntl(); + const [filter, setFilter] = useState({ + id: 'type', + value: [], + }); + + function filterRows(value: string) { + setFilter((prev) => { + if ((prev.value as string[]).includes(value)) { + return { + ...prev, + value: [...(prev.value as string[]).filter((val) => val !== value)], + }; + } else { + return { + ...prev, + value: [...(prev.value as string[]), value], + }; + } + }); + } + return ( + + History + + {[ + intl.formatMessage({ defaultMessage: 'Received' }), + intl.formatMessage({ defaultMessage: 'Sent' }), + intl.formatMessage({ defaultMessage: 'Swap' }), + intl.formatMessage({ defaultMessage: 'Yield' }), + ].map((label) => ( + filterRows(label.toLowerCase())} + > + {label} + + ))} + + + {intl.formatMessage({ defaultMessage: 'Export CSV' })} + + + } + > + {isConnected ? ( + + ) : ( + + + {intl.formatMessage({ + defaultMessage: 'Connect your wallet to see your history', + })} + + + + )} + + ); +} diff --git a/libs/defi/oeth/src/components/History/HistoryTable.stories.tsx b/libs/defi/oeth/src/components/History/HistoryTable.stories.tsx new file mode 100644 index 000000000..dc19da82b --- /dev/null +++ b/libs/defi/oeth/src/components/History/HistoryTable.stories.tsx @@ -0,0 +1,116 @@ +import { useState } from 'react'; + +import { faker } from '@faker-js/faker'; +import { Container, Stack, Typography } from '@mui/material'; +import { within } from '@storybook/testing-library'; + +import { HistoryFilterButton } from './HistoryButton'; +import { HistoryTable } from './HistoryTable'; + +import type { Meta, StoryObj } from '@storybook/react'; +import type { ColumnFilter } from '@tanstack/react-table'; + +import type { HistoryRow } from './HistoryTable'; + +const rows = faker.helpers.multiple( + () => ({ + date: faker.date.recent({ days: 35 }), + balance: faker.number.float({ + min: 10000, + max: 10000000000, + precision: 10, + }), + change: faker.number.float({ min: -10, max: 25 }), + type: faker.helpers.arrayElement(['swap', 'received', 'sent', 'yield']), + link: faker.internet.url(), + }), + { count: 150 }, +); + +const WithFilters = () => { + const [filter, setFilter] = useState({ id: 'type', value: [] }); + return ( + + + History + + {['Received', 'Sent', 'Swap', 'Yield'].map((label) => ( + + setFilter((prev) => { + const filter = label.toLowerCase(); + if ((prev.value as string[]).includes(filter)) { + return { + ...prev, + value: [ + ...(prev.value as string[]).filter( + (val) => val !== filter, + ), + ], + }; + } else { + return { + ...prev, + value: [...(prev.value as string[]), filter], + }; + } + }) + } + circle + > + {label} + + ))} + + + + + ); +}; + +const meta: Meta = { + component: HistoryTable, + title: 'History/History table', + args: { + isLoading: false, + rows, + }, + render: (args) => ( + + + + ), +}; +export default meta; + +export const Primary = { + args: {}, +}; + +export const Filter: StoryObj = { + render: () => ( + + + + ), +}; + +export const SelectedFilters: StoryObj = { + render: () => ( + + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByText('Received').click(); + await canvas.getByText('Yield').click(); + }, +}; diff --git a/libs/defi/oeth/src/components/History/HistoryTable.tsx b/libs/defi/oeth/src/components/History/HistoryTable.tsx new file mode 100644 index 000000000..2401bb7dc --- /dev/null +++ b/libs/defi/oeth/src/components/History/HistoryTable.tsx @@ -0,0 +1,166 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { + Box, + Pagination, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from '@mui/material'; +import { LinkIcon, quantityFormat } from '@origin/shared/components'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { useIntl } from 'react-intl'; + +import type { ColumnFilter, ColumnFiltersState } from '@tanstack/react-table'; + +type Filter = 'swap' | 'yield' | 'received' | 'sent'; + +export interface HistoryRow { + date: Date; + type: Filter; + change: number; + balance: number; + link: string; +} + +interface Props { + rows: HistoryRow[]; + isLoading: boolean; + filter: ColumnFilter; +} + +const columnHelper = createColumnHelper(); + +export function HistoryTable({ rows, filter }: Props) { + const intl = useIntl(); + const [columnFilters, setColumnFilters] = useState([]); + const columns = useMemo( + () => [ + columnHelper.accessor('date', { + cell: (info) => intl.formatDate(info.getValue()), + header: intl.formatMessage({ defaultMessage: 'Date' }), + }), + columnHelper.accessor('type', { + id: 'type', + cell: (info) => info.getValue(), + header: intl.formatMessage({ defaultMessage: 'Type' }), + enableColumnFilter: true, + filterFn: (row, _, value) => { + if (!value.value.length) return true; + return value.value.includes(row.original.type); + }, + }), + columnHelper.accessor('change', { + cell: (info) => intl.formatNumber(info.getValue(), quantityFormat), + header: intl.formatMessage({ defaultMessage: 'Change' }), + }), + columnHelper.accessor('balance', { + cell: (info) => ( + + + {intl.formatNumber(info.getValue(), quantityFormat)} + + + + + ), + header: intl.formatMessage({ defaultMessage: 'OETH Balance' }), + }), + ], + [intl], + ); + + const table = useReactTable({ + data: rows, + columns, + state: { + pagination: { + pageSize: 20, + pageIndex: 0, + }, + columnFilters, + }, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnFiltersChange: setColumnFilters, + // add when we do server side pagination + // manualPagination: true, + pageCount: rows.length / 3, + // add when we do server side pagination + // onPaginationChange: setPagination + }); + + useEffect(() => { + table.getColumn('type')?.setFilterValue(filter); + }, [filter, table]); + return ( + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+ table.setPageIndex(page)} + /> +
+ ); +} diff --git a/libs/defi/oeth/src/components/History/index.tsx b/libs/defi/oeth/src/components/History/index.tsx new file mode 100644 index 000000000..27e462d84 --- /dev/null +++ b/libs/defi/oeth/src/components/History/index.tsx @@ -0,0 +1 @@ +export * from './HistoryCard'; diff --git a/libs/defi/oeth/src/components/Swap/Swap.tsx b/libs/defi/oeth/src/components/Swap/Swap.tsx index 5d1b6583d..6850604fb 100644 --- a/libs/defi/oeth/src/components/Swap/Swap.tsx +++ b/libs/defi/oeth/src/components/Swap/Swap.tsx @@ -104,7 +104,9 @@ export function Swap() { } > - console.log('test')}>Swap + console.log('test')}> + {intl.formatMessage({ defaultMessage: 'Swap' })} + = { - component: DefiOeth, - title: 'DefiOeth', -}; -export default Story; - -export const Primary = { - args: {}, -}; diff --git a/libs/defi/oeth/src/components/index.tsx b/libs/defi/oeth/src/components/index.tsx new file mode 100644 index 000000000..b99afaa6d --- /dev/null +++ b/libs/defi/oeth/src/components/index.tsx @@ -0,0 +1,4 @@ +export * from './shared'; +export * from './Swap'; +export * from './Wrap'; +export * from './History'; diff --git a/libs/defi/oeth/src/components/shared/APY.stories.tsx b/libs/defi/oeth/src/components/shared/APY.stories.tsx new file mode 100644 index 000000000..25356ef12 --- /dev/null +++ b/libs/defi/oeth/src/components/shared/APY.stories.tsx @@ -0,0 +1,48 @@ +import { Container } from '@mui/material'; + +import { APY } from './APY'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: APY, + title: 'OETH/APY', + args: { + tokenIcon: ' https://app.oeth.com/images/oeth.svg', + value: 8.71, + balance: 250.1937, + pendingYield: 0.0023, + earnings: 15.1937, + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const SmallMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; +export const LargeMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; +export const Tablet: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'tablet', + }, + }, +}; diff --git a/libs/defi/oeth/src/components/shared/APY.tsx b/libs/defi/oeth/src/components/shared/APY.tsx new file mode 100644 index 000000000..34a7a9307 --- /dev/null +++ b/libs/defi/oeth/src/components/shared/APY.tsx @@ -0,0 +1,240 @@ +import React, { useState } from 'react'; + +import { + alpha, + Box, + Divider, + IconButton, + Menu, + MenuItem, + Stack, + Typography, +} from '@mui/material'; +import { Icon } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; + +const days = [7, 30]; + +interface Props { + value: number; + tokenIcon: string; + balance: number; + pendingYield: number; + earnings: number; +} + +export function APY({ + value, + tokenIcon, + balance, + pendingYield, + earnings, +}: Props) { + const intl = useIntl(); + const [selectedPeriod, setSelectedPeriod] = useState(30); + const [anchorEl, setAnchorEl] = React.useState(null); + + function handleClose() { + setAnchorEl(null); + } + + return ( + <> + + {days.map((day) => ( + { + setSelectedPeriod(day); + setAnchorEl(null); + }} + > + {intl.formatMessage( + { defaultMessage: '{days} day trailing' }, + { days: day }, + )} + + ))} + + + + + {intl.formatMessage( + { defaultMessage: '{days} day trailing APY' }, + { days: selectedPeriod }, + )} + + + + {intl.formatNumber(value / 100, { + minimumFractionDigits: 2, + style: 'percent', + })} + + setAnchorEl(e.currentTarget)} + sx={{ + backgroundColor: (theme) => + alpha(theme.palette.common.white, 0.15), + marginInlineStart: 1, + alignSelf: 'center', + position: 'relative', + height: '26px', + borderRadius: '100%', + top: '-2px', + }} + > + + + + + + + + + + + + + + + + + + + ); +} + +function ValueContainer({ + text, + value, + icon, +}: { + text: string; + value: string; + icon?: string; +}) { + return ( + + + {text} + + + {icon ? ( + + ) : undefined} + + {value} + + + ); +} diff --git a/libs/defi/oeth/src/constants.ts b/libs/defi/oeth/src/constants.ts new file mode 100644 index 000000000..2337e9e1d --- /dev/null +++ b/libs/defi/oeth/src/constants.ts @@ -0,0 +1,11 @@ +import { FormatNumberOptions } from 'react-intl'; + +export const numberCurrencyFormat: FormatNumberOptions = { + minimumFractionDigits: 2, + style: 'currency', + currency: 'USD', +}; + +export const valueFormat: FormatNumberOptions = { + minimumFractionDigits: 2, +}; diff --git a/libs/defi/oeth/src/index.ts b/libs/defi/oeth/src/index.ts index 1147309dc..792962380 100644 --- a/libs/defi/oeth/src/index.ts +++ b/libs/defi/oeth/src/index.ts @@ -1 +1 @@ -export * from './components/defi-oeth'; +export * from './views'; diff --git a/libs/defi/oeth/src/views/Wrap.tsx b/libs/defi/oeth/src/views/Wrap.tsx index 3adce7712..54ba19d6a 100644 --- a/libs/defi/oeth/src/views/Wrap.tsx +++ b/libs/defi/oeth/src/views/Wrap.tsx @@ -1,9 +1,8 @@ import { Button, Stack } from '@mui/material'; -import { Card } from '@origin/shared/components'; +import { ActionButton, Card } from '@origin/shared/components'; import { useIntl } from 'react-intl'; import { APY, PortfolioSwap } from '../components'; -import { ConnectWallet } from '../components/shared'; export function WrapView() { const intl = useIntl(); @@ -48,7 +47,9 @@ export function WrapView() { - + console.log('test')}> + {intl.formatMessage({ defaultMessage: 'Connect' })} + );