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 (
+ <>
+
+
+
+
+ {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/components/shared/index.tsx b/libs/defi/oeth/src/components/shared/index.tsx
index e8593a84b..fa1f0843c 100644
--- a/libs/defi/oeth/src/components/shared/index.tsx
+++ b/libs/defi/oeth/src/components/shared/index.tsx
@@ -1,2 +1 @@
-export * from './ConnectButton';
export * from './APY';
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' })}
+
>
);