diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..715a50937 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: [push] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: latest + cache: 'yarn' + + - name: Install Dependencies + run: yarn install --frozen-lockfile + + - name: Code format check + run: yarn format:check + + - name: Linting + run: yarn lint + + - name: Run Tests + run: yarn test:ci diff --git a/.gitignore b/.gitignore index a97942198..d6b405861 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist node_modules storybook-static +coverage diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..039f1d4aa --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +.next +dist +node_modules +.storybook +graphql +contracts.ts +**/rollups-wagmi/src/index.tsx \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 1fe8b7ddf..f904768b8 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,5 @@ { - "tabWidth": 4, + "tabWidth": 4, "overrides": [ { "files": "*.md", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..94a8a7271 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } +} \ No newline at end of file diff --git a/apps/web/src/app/applications/[address]/page.tsx b/apps/web/src/app/applications/[address]/page.tsx index f01d06c34..45a7916bf 100644 --- a/apps/web/src/app/applications/[address]/page.tsx +++ b/apps/web/src/app/applications/[address]/page.tsx @@ -1,8 +1,4 @@ "use client"; -import { FC, useEffect, useState } from "react"; -import { useScrollIntoView } from "@mantine/hooks"; -import { pathOr } from "ramda"; -import { TbInbox } from "react-icons/tb"; import { Anchor, Breadcrumbs, @@ -14,14 +10,18 @@ import { Text, Title, } from "@mantine/core"; +import { useScrollIntoView } from "@mantine/hooks"; import Link from "next/link"; -import { InputOrderByInput, useInputsQuery } from "../../../graphql"; +import { pathOr } from "ramda"; +import { FC, useEffect, useState } from "react"; +import { TbInbox } from "react-icons/tb"; import Address from "../../../components/address"; +import InputRow from "../../../components/inputRow"; +import { InputOrderByInput, useInputsQuery } from "../../../graphql"; import { limitBounds, usePaginationParams, } from "../../../hooks/usePaginationParams"; -import InputRow from "../../../components/inputRow"; export type ApplicationPageProps = { params: { address: string }; diff --git a/apps/web/src/components/inputRow.tsx b/apps/web/src/components/inputRow.tsx index 03a524e06..81da35388 100644 --- a/apps/web/src/components/inputRow.tsx +++ b/apps/web/src/components/inputRow.tsx @@ -1,11 +1,11 @@ "use client"; +import { erc20PortalAddress, etherPortalAddress } from "@cartesi/rollups-wagmi"; import { ActionIcon, Badge, Collapse, Group, JsonInput, - Stack, Table, Tabs, Text, @@ -22,7 +22,6 @@ import { TbX, } from "react-icons/tb"; import { Hex, formatUnits, getAddress, hexToString } from "viem"; -import { erc20PortalAddress, etherPortalAddress } from "@cartesi/rollups-wagmi"; import { InputItemFragment } from "../graphql"; import Address from "./address"; diff --git a/apps/workshop/.eslintrc.cjs b/apps/workshop/.eslintrc.cjs index c9ad94a5f..8663fef60 100644 --- a/apps/workshop/.eslintrc.cjs +++ b/apps/workshop/.eslintrc.cjs @@ -1,4 +1,7 @@ module.exports = { root: true, extends: ["cartesi"], + rules: { + "@next/next/no-img-element": 'off' + } }; diff --git a/apps/workshop/.storybook/main.ts b/apps/workshop/.storybook/main.ts index 5c24c9b89..577191919 100644 --- a/apps/workshop/.storybook/main.ts +++ b/apps/workshop/.storybook/main.ts @@ -10,6 +10,7 @@ const config: StorybookConfig = { "@storybook/addon-interactions", "@storybook/addon-styling", "storybook-dark-mode", + "@storybook/addon-viewport", ], docs: { autodocs: "tag", diff --git a/apps/workshop/README.md b/apps/workshop/README.md index e57168e1d..1ebe379f5 100644 --- a/apps/workshop/README.md +++ b/apps/workshop/README.md @@ -4,14 +4,14 @@ This template provides a minimal setup to get React working in Vite with HMR and Currently, two official plugins are available: -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: -- Configure the top-level `parserOptions` property like this: +- Configure the top-level `parserOptions` property like this: ```js parserOptions: { @@ -22,6 +22,6 @@ If you are developing a production application, we recommend updating the config }, ``` -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/apps/workshop/package.json b/apps/workshop/package.json index df5652e33..9cbe1b9ba 100644 --- a/apps/workshop/package.json +++ b/apps/workshop/package.json @@ -27,6 +27,7 @@ "@storybook/addon-links": "^7.4.0", "@storybook/addon-onboarding": "^1.0.8", "@storybook/addon-styling": "^1.3.7", + "@storybook/addon-viewport": "^7.4.0", "@storybook/blocks": "^7.4.0", "@storybook/cli": "^7.4.0", "@storybook/preview-api": "^7.4.0", diff --git a/apps/workshop/src/App.tsx b/apps/workshop/src/App.tsx index 4c3373952..b62f7b3ac 100644 --- a/apps/workshop/src/App.tsx +++ b/apps/workshop/src/App.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; +import "./App.css"; import reactLogo from "./assets/react.svg"; import viteLogo from "/vite.svg"; -import "./App.css"; function App() { const [count, setCount] = useState(0); diff --git a/apps/workshop/src/stories/InputDetails.stories.tsx b/apps/workshop/src/stories/InputDetails.stories.tsx new file mode 100644 index 000000000..fb15313a6 --- /dev/null +++ b/apps/workshop/src/stories/InputDetails.stories.tsx @@ -0,0 +1,301 @@ +import { + InputContent, + InputDetails, + NoticeContent, + ReportContent, + VoucherContent, +} from "@cartesi/rollups-explorer-ui"; +import { Button, Group, Stack, Title } from "@mantine/core"; +import type { Meta, StoryObj } from "@storybook/react"; +import { useCallback, useEffect, useState } from "react"; + +const meta: Meta = { + component: InputDetails, +}; + +export default meta; +type Story = StoryObj; + +const reportEx = "0x6f696969"; +const jsonWithdraw0 = + "0x7b0a20202020226d6574686f64223a202265726332307769746864726177616c222c0a202020202261726773223a207b0a2020202020202020226572633230223a2022307830353963373530374239373364313531323736386330366633326138313342433933443833454232222c0a202020202020202022616d6f756e74223a203130300a202020207d0a7d"; +const jsonWithdraw1 = + "0x7b0a20202020226d6574686f64223a202265726332307769746864726177616c222c0a202020202261726773223a207b0a2020202020202020226572633230223a2022307835396236373065396641394430413432373735314166323031443637363731396139373038353762222c0a202020202020202022616d6f756e74223a203130300a202020207d0a7d"; +const jsonContent = + "0x7b0a20202020226d6574686f64223a202265726332307769746864726177616c222c0a202020202261726773223a207b0a2020202020202020226572633230223a2022307835396236373065396641394430413432373735314166323031443637363731396139373038353762222c0a202020202020202022616d6f756e74223a2031303030303030303030303030303030303030300a202020207d0a7d"; +const queryContent = `0x494e5345525420494e544f20636572746966696572202056414c554553202827307866434432423566316346353562353643306632323464614439394331346234454530393237346433272c3130202c273078664344324235663163463535623536433066323234646144393943313462344545303932373464332729`; + +const useEmulatedData = (dataList: string[], delay = 0) => { + const [loading, setLoading] = useState(delay === 0 ? false : true); + const [index, setIndex] = useState(0); + const data = loading ? "" : dataList[index]; + + const updateHooks = useCallback( + (action: () => void) => { + if (delay > 0) { + setLoading(true); + setTimeout(() => { + action(); + setLoading(false); + }, delay); + } else { + action(); + } + }, + [setLoading, delay], + ); + + const nextPage = () => { + updateHooks(() => setIndex((n) => n + 1)); + }; + + const prevPage = () => { + updateHooks(() => setIndex((n) => n - 1)); + }; + + useEffect(() => { + if (delay > 0) { + updateHooks(() => setIndex(0)); + } + }, [delay, updateHooks]); + + return { + total: dataList.length, + data, + loading, + nextPage, + prevPage, + }; +}; + +const InputDetailsWithDelay = () => { + const reportQuery = useEmulatedData([reportEx], 1000); + const voucherQuery = useEmulatedData( + [jsonWithdraw0, jsonWithdraw1, jsonContent], + 1500, + ); + const noticeQuery = useEmulatedData([queryContent], 300); + + return ( + + + reportQuery.nextPage(), + onPreviousPage: () => reportQuery.prevPage(), + total: reportQuery.total, + }} + /> + voucherQuery.nextPage(), + onPreviousPage: () => voucherQuery.prevPage(), + total: voucherQuery.total, + }} + /> + noticeQuery.nextPage(), + onPreviousPage: () => noticeQuery.prevPage(), + total: noticeQuery.total, + }} + /> + + ); +}; + +const InputDetailsWithHooks = () => { + const reportQuery = useEmulatedData([reportEx]); + const voucherQuery = useEmulatedData([ + jsonWithdraw0, + jsonWithdraw1, + jsonContent, + ]); + const noticeQuery = useEmulatedData([queryContent]); + + return ( + + + reportQuery.nextPage(), + onPreviousPage: () => reportQuery.prevPage(), + total: reportQuery.total, + }} + /> + voucherQuery.nextPage(), + onPreviousPage: () => voucherQuery.prevPage(), + total: voucherQuery.total, + }} + /> + noticeQuery.nextPage(), + onPreviousPage: () => noticeQuery.prevPage(), + total: noticeQuery.total, + }} + /> + + ); +}; + +const InputDetailsNoContent = () => { + const voucherQuery = useEmulatedData([], 1000); + + return ( + + + voucherQuery.nextPage(), + onPreviousPage: () => voucherQuery.prevPage(), + total: voucherQuery.total, + }} + /> + + ); +}; + +const WithDynamicContent = () => { + const reportQuery = useEmulatedData([reportEx]); + const voucherQuery = useEmulatedData([ + jsonWithdraw0, + jsonWithdraw1, + jsonContent, + ]); + const noticeQuery = useEmulatedData([queryContent]); + + const [showNotice, setShowNotice] = useState(false); + const [showReport, setShowReport] = useState(true); + const [showVourcher, setShowVourcher] = useState(false); + + return ( + + + Toggle the content and watch the tabs change + + + + + + + + + + {showReport && ( + reportQuery.nextPage(), + onPreviousPage: () => reportQuery.prevPage(), + total: reportQuery.total, + }} + /> + )} + + {showVourcher && ( + voucherQuery.nextPage(), + onPreviousPage: () => voucherQuery.prevPage(), + total: voucherQuery.total, + }} + /> + )} + + {showNotice && ( + noticeQuery.nextPage(), + onPreviousPage: () => noticeQuery.prevPage(), + total: noticeQuery.total, + }} + /> + )} + + + ); +}; + +const WithActionToConnect = () => { + const [delay, setDelay] = useState(0); + const voucherQuery = useEmulatedData( + [jsonWithdraw0, jsonWithdraw1, jsonContent], + delay, + ); + const [isConnected, setIsConnected] = useState(false); + + return ( + + + { + setDelay(600); + setIsConnected(() => { + return true; + }); + }} + content={voucherQuery.data} + isLoading={voucherQuery.loading} + contentType="raw" + paging={{ + onNextPage: () => voucherQuery.nextPage(), + onPreviousPage: () => voucherQuery.prevPage(), + total: voucherQuery.total, + }} + /> + + ); +}; + +export const Default: Story = { + render: () => , +}; + +export const WithDelay: Story = { + render: () => , +}; + +export const VoucherWithNoContent = { + render: () => , +}; + +export const DynamicDisplayingContent = { + render: () => , +}; + +export const WithConnectAction = { + render: () => , +}; diff --git a/package.json b/package.json index 326531629..6606cdc4e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "dev": "turbo run dev --no-cache --continue", "lint": "turbo run lint", "clean": "turbo run clean && rm -rf node_modules", + "format:check": "prettier \"**/*.{ts,tsx}\" --check", "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "test" : "turbo run test", + "test:ci" : "turbo run test:ci", "changeset": "changeset", "version-packages": "changeset version", "release": "turbo run build && changeset publish" @@ -19,7 +22,7 @@ "devDependencies": { "@changesets/cli": "^2.26.2", "eslint-config-cartesi": "*", - "prettier": "^3.0.3", + "prettier": "3.0.3", "turbo": "^1.10.13" }, "engines": { diff --git a/packages/rollups-wagmi/package.json b/packages/rollups-wagmi/package.json index f7349ecb7..d44e952a2 100644 --- a/packages/rollups-wagmi/package.json +++ b/packages/rollups-wagmi/package.json @@ -13,8 +13,7 @@ "build": "tsup", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", "codegen": "wagmi generate", - "dev": "tsup --watch", - "lint": "eslint \"src/**/*.ts*\"" + "dev": "tsup --watch" }, "dependencies": { "wagmi": "^1" diff --git a/packages/ui/package.json b/packages/ui/package.json index 3983ac51b..72891dc9b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,13 +13,18 @@ "build": "tsup", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", "dev": "tsup --watch", - "lint": "eslint \"src/**/*.ts*\"" + "lint": "eslint \"src/**/*.ts*\"", + "test": "vitest run", + "test:watch": "vitest", + "test:ci": "vitest run --coverage" }, "dependencies": { "@mantine/core": "^7.0.0", "@mantine/hooks": "^7.0.0", "@react-spring/web": "^9.7.3", - "react-icons": "^4" + "ramda": "^0.29.0", + "react-icons": "^4", + "viem": "^1" }, "peerDependencies": { "@mantine/core": "^7.0.0", @@ -27,13 +32,19 @@ }, "devDependencies": { "@cartesi/tsconfig": "*", + "@testing-library/jest-dom": "^6.1.3", + "@testing-library/react": "^14.0.0", + "@types/ramda": "^0.29.3", "@types/react": "^18", "@types/react-dom": "^18", + "@vitest/coverage-v8": "^0.34.5", "eslint": "^8", "eslint-config-cartesi": "*", + "jsdom": "^22.1.0", "react": "^18", "tsup": "^7", - "typescript": "^5" + "typescript": "^5", + "vitest": "^0.34.5" }, "publishConfig": { "access": "public" diff --git a/packages/ui/src/InputDetails/Content.tsx b/packages/ui/src/InputDetails/Content.tsx new file mode 100644 index 000000000..3ad3343d9 --- /dev/null +++ b/packages/ui/src/InputDetails/Content.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { JsonInput, SegmentedControl, Textarea } from "@mantine/core"; +import { T, cond, equals, pipe, propOr } from "ramda"; +import { Hex, hexToString } from "viem"; + +export interface ContentProps { + content: string; + contentType: ContentType; +} + +export type ContentType = "raw" | "text" | "json"; + +interface ContentTypeGroupedButtons { + type: ContentType; + onTypeChange: (v: ContentType) => void; +} + +export const ContentTypeControl = ({ + type, + onTypeChange, +}: ContentTypeGroupedButtons) => { + return ( + onTypeChange(v)} + data={[ + { label: "Raw", value: "raw" }, + { label: "As Text", value: "text" }, + { label: "As JSON", value: "json" }, + ]} + /> + ); +}; + +export const DisplayContent = cond< + [props: { type?: ContentType; content: string }], + JSX.Element +>([ + [ + pipe(propOr("", "type"), equals("json")), + ({ content }) => ( + + ), + ], + [ + pipe(propOr("", "type"), equals("text")), + ({ content }) => ( +