瀏覽代碼

chore(pyth-pro-perf-dash): start of more formal dash

benduran 4 天之前
父節點
當前提交
915ef18f5b
共有 36 個文件被更改,包括 756 次插入0 次删除
  1. 133 0
      apps/pyth-pro-perf-dash/.gitignore
  2. 14 0
      apps/pyth-pro-perf-dash/.prettierignore
  3. 13 0
      apps/pyth-pro-perf-dash/README.md
  4. 1 0
      apps/pyth-pro-perf-dash/eslint.config.js
  5. 3 0
      apps/pyth-pro-perf-dash/jest.config.js
  6. 6 0
      apps/pyth-pro-perf-dash/next-env.d.ts
  7. 55 0
      apps/pyth-pro-perf-dash/next.config.js
  8. 66 0
      apps/pyth-pro-perf-dash/package.json
  9. 5 0
      apps/pyth-pro-perf-dash/postcss.config.js
  10. 1 0
      apps/pyth-pro-perf-dash/prettier.config.js
  11. 二進制
      apps/pyth-pro-perf-dash/public/android-chrome-192x192.png
  12. 二進制
      apps/pyth-pro-perf-dash/public/android-chrome-512x512.png
  13. 二進制
      apps/pyth-pro-perf-dash/public/apple-touch-icon.png
  14. 二進制
      apps/pyth-pro-perf-dash/public/favicon-16x16.png
  15. 二進制
      apps/pyth-pro-perf-dash/public/favicon-32x32.png
  16. 二進制
      apps/pyth-pro-perf-dash/public/favicon-light.ico
  17. 二進制
      apps/pyth-pro-perf-dash/public/favicon.ico
  18. 11 0
      apps/pyth-pro-perf-dash/router.d.ts
  19. 13 0
      apps/pyth-pro-perf-dash/src/app/(main)/page.module.scss
  20. 14 0
      apps/pyth-pro-perf-dash/src/app/(main)/page.tsx
  21. 20 0
      apps/pyth-pro-perf-dash/src/app/globals.scss
  22. 44 0
      apps/pyth-pro-perf-dash/src/app/layout.tsx
  23. 1 0
      apps/pyth-pro-perf-dash/src/components/PriceCard/index.ts
  24. 3 0
      apps/pyth-pro-perf-dash/src/components/PriceCard/price-card.tsx
  25. 1 0
      apps/pyth-pro-perf-dash/src/components/SourcePicker/index.ts
  26. 12 0
      apps/pyth-pro-perf-dash/src/components/SourcePicker/source-picker.module.scss
  27. 36 0
      apps/pyth-pro-perf-dash/src/components/SourcePicker/source-picker.tsx
  28. 2 0
      apps/pyth-pro-perf-dash/src/components/index.ts
  29. 49 0
      apps/pyth-pro-perf-dash/src/constants/index.ts
  30. 26 0
      apps/pyth-pro-perf-dash/src/state/ui-state.ts
  31. 45 0
      apps/pyth-pro-perf-dash/src/types/index.ts
  32. 6 0
      apps/pyth-pro-perf-dash/svg.d.ts
  33. 5 0
      apps/pyth-pro-perf-dash/tsconfig.json
  34. 36 0
      apps/pyth-pro-perf-dash/turbo.json
  35. 4 0
      apps/pyth-pro-perf-dash/vercel.json
  36. 131 0
      pnpm-lock.yaml

+ 133 - 0
apps/pyth-pro-perf-dash/.gitignore

@@ -0,0 +1,133 @@
+.env*.local
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.*
+!.env.example
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Sveltekit cache directory
+.svelte-kit/
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# Firebase cache directory
+.firebase/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v3
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+# Vite logs files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*

+ 14 - 0
apps/pyth-pro-perf-dash/.prettierignore

@@ -0,0 +1,14 @@
+.next/
+coverage/
+node_modules/
+*.tsbuildinfo
+.env*.local
+.env
+.DS_Store
+dist/
+lib/
+build/
+node_modules/
+package.json
+tsconfig*.json
+turbo.json

+ 13 - 0
apps/pyth-pro-perf-dash/README.md

@@ -0,0 +1,13 @@
+# @pythnetwork/pyth-pro-perf-dash
+
+A dashboard that contains some visualizations for monitoring latency and price feeds, as compared to other popular sources
+
+TODO: Fill out readme with more instructions
+
+## Running locally
+
+From the root of this repo:
+
+```
+pnpm turbo run start:dev --filter ./apps/pyth-pro-perf-dash
+```

+ 1 - 0
apps/pyth-pro-perf-dash/eslint.config.js

@@ -0,0 +1 @@
+export { nextjs as default } from "@cprussin/eslint-config";

+ 3 - 0
apps/pyth-pro-perf-dash/jest.config.js

@@ -0,0 +1,3 @@
+import { defineJestConfigForNextJs } from "@pythnetwork/jest-config/define-next-config";
+
+export default defineJestConfigForNextJs();

+ 6 - 0
apps/pyth-pro-perf-dash/next-env.d.ts

@@ -0,0 +1,6 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+/// <reference path="./.next/types/routes.d.ts" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 55 - 0
apps/pyth-pro-perf-dash/next.config.js

@@ -0,0 +1,55 @@
+export default {
+  reactStrictMode: true,
+
+  pageExtensions: ["ts", "tsx", "mdx"],
+
+  logging: {
+    fetches: {
+      fullUrl: true,
+    },
+  },
+
+  webpack(config) {
+    config.module.rules.push({
+      test: /\.svg$/i,
+      use: ["@svgr/webpack"],
+    });
+
+    config.resolve.extensionAlias = {
+      ".js": [".js", ".ts", ".tsx"],
+    };
+
+    return config;
+  },
+
+  headers: () => [
+    {
+      source: "/:path*",
+      headers: [
+        {
+          key: "X-XSS-Protection",
+          value: "1; mode=block",
+        },
+        {
+          key: "Referrer-Policy",
+          value: "strict-origin-when-cross-origin",
+        },
+        {
+          key: "Strict-Transport-Security",
+          value: "max-age=2592000",
+        },
+        {
+          key: "X-Content-Type-Options",
+          value: "nosniff",
+        },
+        {
+          key: "Permissions-Policy",
+          value:
+            "vibrate=(), geolocation=(), midi=(), notifications=(), push=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), speaker=(), vibrate=(), fullscreen=self",
+        },
+      ],
+    },
+  ],
+
+  rewrites: () => [],
+};

+ 66 - 0
apps/pyth-pro-perf-dash/package.json

@@ -0,0 +1,66 @@
+{
+  "private": true,
+  "name": "@pythnetwork/pyth-pro-perf-dash",
+  "description": "A dashboard that contains some visualizations for monitoring latency and price feeds, as compared to other popular sources",
+  "version": "0.0.0",
+  "type": "module",
+  "engines": {
+    "node": ">=22.14.0"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/pyth-network/pyth-crosschain",
+    "directory": "apps/pyth-pro-perf-dash"
+  },
+  "scripts": {
+    "build:vercel": "next build",
+    "fix:format": "prettier --write .",
+    "fix:lint:eslint": "eslint --fix .",
+    "fix:lint:stylelint": "stylelint --fix 'src/**/*.scss'",
+    "pull:env": "echo 'once @pythnetwork/pyth-pro-perf-dash has been setup in the Vercel UI, you can replace this script with the contents in the pull:env:placeholder, below, as well as the VERCEL_PROJECT_ID variable value'",
+    "pull:env:placeholder": "[ $CI ] || VERCEL_ORG_ID=team_BKQrg3JJFLxZyTqpuYtIY0rj VERCEL_PROJECT_ID=TODO_FILL_ME_IN vercel env pull",
+    "start:dev": "next dev --port 9876",
+    "start:prod": "next start --port 9876",
+    "test:format": "prettier --check .",
+    "test:lint:eslint": "eslint . --max-warnings 0",
+    "test:lint:stylelint": "stylelint 'src/**/*.scss' --max-warnings 0",
+    "test:types": "tsc",
+    "test:unit": "test-unit"
+  },
+  "devDependencies": {
+    "@axe-core/react": "catalog:",
+    "@cprussin/eslint-config": "catalog:",
+    "@cprussin/prettier-config": "catalog:",
+    "@cprussin/tsconfig": "catalog:",
+    "@pythnetwork/jest-config": "workspace:",
+    "@types/jest": "catalog:",
+    "@types/node": "catalog:",
+    "@types/react": "catalog:",
+    "@types/react-dom": "catalog:",
+    "autoprefixer": "catalog:",
+    "eslint": "catalog:",
+    "jest": "catalog:",
+    "postcss": "catalog:",
+    "prettier": "catalog:",
+    "type-fest": "catalog:",
+    "vercel": "catalog:"
+  },
+  "dependencies": {
+    "@next/third-parties": "catalog:",
+    "@pythnetwork/component-library": "workspace:*",
+    "@pythnetwork/react-hooks": "workspace:*",
+    "@react-hookz/web": "catalog:",
+    "clsx": "catalog:",
+    "framer-motion": "catalog:",
+    "next": "catalog:",
+    "nuqs": "catalog:",
+    "pino": "catalog:",
+    "react": "catalog:",
+    "react-aria": "catalog:",
+    "react-aria-components": "catalog:",
+    "react-dom": "catalog:",
+    "swr": "catalog:",
+    "zod": "catalog:",
+    "zustand": "^5.0.8"
+  }
+}

+ 5 - 0
apps/pyth-pro-perf-dash/postcss.config.js

@@ -0,0 +1,5 @@
+export default {
+  plugins: {
+    autoprefixer: {},
+  },
+};

+ 1 - 0
apps/pyth-pro-perf-dash/prettier.config.js

@@ -0,0 +1 @@
+export { base as default } from "@cprussin/prettier-config";

二進制
apps/pyth-pro-perf-dash/public/android-chrome-192x192.png


二進制
apps/pyth-pro-perf-dash/public/android-chrome-512x512.png


二進制
apps/pyth-pro-perf-dash/public/apple-touch-icon.png


二進制
apps/pyth-pro-perf-dash/public/favicon-16x16.png


二進制
apps/pyth-pro-perf-dash/public/favicon-32x32.png


二進制
apps/pyth-pro-perf-dash/public/favicon-light.ico


二進制
apps/pyth-pro-perf-dash/public/favicon.ico


+ 11 - 0
apps/pyth-pro-perf-dash/router.d.ts

@@ -0,0 +1,11 @@
+import "react-aria-components";
+
+declare module "react-aria-components" {
+  import { useRouter } from "next/navigation";
+
+  export type RouterConfig = {
+    routerOptions: NonNullable<
+      Parameters<ReturnType<typeof useRouter>["push"]>[1]
+    >;
+  };
+}

+ 13 - 0
apps/pyth-pro-perf-dash/src/app/(main)/page.module.scss

@@ -0,0 +1,13 @@
+@use "@pythnetwork/component-library/theme";
+
+.main {
+  @include theme.max-width;
+
+  padding: theme.spacing(4) 0;
+}
+
+.top {
+  align-items: center;
+  display: flex;
+  gap: theme.spacing(2);
+}

+ 14 - 0
apps/pyth-pro-perf-dash/src/app/(main)/page.tsx

@@ -0,0 +1,14 @@
+import classes from "./page.module.scss";
+import { SourcePicker } from "../../components";
+
+export default function Home() {
+  return (
+    <div>
+      <main className={classes.main}>
+        <div className={classes.top}>
+          <SourcePicker />
+        </div>
+      </main>
+    </div>
+  );
+}

+ 20 - 0
apps/pyth-pro-perf-dash/src/app/globals.scss

@@ -0,0 +1,20 @@
+:root {
+  --background: #ffffff;
+  --foreground: #171717;
+
+  --font-sans: "Geist Sans", Arial, Helvetica, sans-serif;
+  --font-mono: "Geist Mono", monospace;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --background: #0a0a0a;
+    --foreground: #ededed;
+  }
+}
+
+body {
+  background: var(--background);
+  color: var(--foreground);
+  font-family: var(--font-sans);
+}

+ 44 - 0
apps/pyth-pro-perf-dash/src/app/layout.tsx

@@ -0,0 +1,44 @@
+import { AppShell } from "@pythnetwork/component-library/AppShell";
+import { NuqsAdapter } from "@pythnetwork/react-hooks/nuqs-adapters-next";
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.scss";
+import type { ReactNode } from "react";
+
+const geistSans = Geist({
+  variable: "--font-geist-sans",
+  subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+  variable: "--font-geist-mono",
+  subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+  title: "Pyth Price Monitor",
+  description:
+    "A dashboard that contains some visualizations for monitoring latency and price feeds, as compared to other popular sources",
+};
+
+export default function RootLayout({
+  children,
+}: Readonly<{
+  children: ReactNode;
+}>) {
+  return (
+    <html lang="en">
+      <body
+        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
+      >
+        <AppShell
+          appName="Pyth Price Monitor"
+          enableAccessibilityReporting
+          providers={[NuqsAdapter]}
+        >
+          {children}
+        </AppShell>
+      </body>
+    </html>
+  );
+}

+ 1 - 0
apps/pyth-pro-perf-dash/src/components/PriceCard/index.ts

@@ -0,0 +1 @@
+export * from "./price-card";

+ 3 - 0
apps/pyth-pro-perf-dash/src/components/PriceCard/price-card.tsx

@@ -0,0 +1,3 @@
+export function PriceCard() {
+  return <div>price card</div>;
+}

+ 1 - 0
apps/pyth-pro-perf-dash/src/components/SourcePicker/index.ts

@@ -0,0 +1 @@
+export * from "./source-picker";

+ 12 - 0
apps/pyth-pro-perf-dash/src/components/SourcePicker/source-picker.module.scss

@@ -0,0 +1,12 @@
+@use "@pythnetwork/component-library/theme";
+
+.sourcePickerRoot {
+  
+}
+
+.sourcePickerSelect {
+  display: flex;
+  flex-flow: column;
+  gap: theme.spacing(1);
+  width: 240px;
+}

+ 36 - 0
apps/pyth-pro-perf-dash/src/components/SourcePicker/source-picker.tsx

@@ -0,0 +1,36 @@
+"use client";
+
+import { Select } from "@pythnetwork/component-library/Select";
+
+import classes from "./source-picker.module.scss";
+import {
+  SOURCE_PICKER_DROPDOWN_GROUPS,
+  SOURCE_PICKER_DROPDOWN_OPTIONS,
+} from "../../constants";
+import { useUIStateStore } from "../../state/ui-state";
+
+export function SourcePicker() {
+  /** store */
+  const selectedSource = useUIStateStore((state) => state.selectedSource);
+  const updateSelectedSource = useUIStateStore(
+    (state) => state.setSelectedSource,
+  );
+
+  return (
+    <div className={classes.sourcePickerRoot}>
+      <Select
+        className={String(classes.sourcePickerSelect)}
+        label="Choose what to monitor"
+        optionGroups={SOURCE_PICKER_DROPDOWN_GROUPS}
+        onSelectionChange={(optId) => {
+          const opt = SOURCE_PICKER_DROPDOWN_OPTIONS.find(
+            (opt) => opt.id === optId,
+          );
+          if (opt) updateSelectedSource(opt);
+        }}
+        selectedKey={selectedSource.id}
+        placeholder="Choose what to monitor"
+      />
+    </div>
+  );
+}

+ 2 - 0
apps/pyth-pro-perf-dash/src/components/index.ts

@@ -0,0 +1,2 @@
+export * from "./PriceCard";
+export * from "./SourcePicker";

+ 49 - 0
apps/pyth-pro-perf-dash/src/constants/index.ts

@@ -0,0 +1,49 @@
+// export const SUPPORTED_CURRENCY_PAIRS = ["EURUSD"] as const;
+// export const SUPPORTED_EQUITIES = ["TSLA"] as const;
+// export const SUPPORTED_CRYPTO_CURRENCIES = ["BTC"] as const;
+
+import type {
+  SourcePickerDropdownGroup,
+  SourcePickerDropdownOption,
+} from "../types";
+import {
+  SUPPORTED_CRYPTO_CURRENCIES,
+  SUPPORTED_CURRENCY_PAIRS,
+  SUPPORTED_EQUITIES,
+} from "../types";
+
+export const SOURCE_PICKER_DROPDOWN_OPTIONS: SourcePickerDropdownOption[] = [
+  ...SUPPORTED_CRYPTO_CURRENCIES.map<SourcePickerDropdownOption>((id) => ({
+    crypto: true,
+    equity: false,
+    forex: false,
+    id,
+  })),
+  ...SUPPORTED_EQUITIES.map<SourcePickerDropdownOption>((id) => ({
+    crypto: false,
+    equity: true,
+    forex: true,
+    id,
+  })),
+  ...SUPPORTED_CURRENCY_PAIRS.map<SourcePickerDropdownOption>((id) => ({
+    crypto: false,
+    equity: false,
+    forex: true,
+    id,
+  })),
+].sort((a, b) => a.id.localeCompare(b.id));
+
+const groupedOpts = Object.groupBy(SOURCE_PICKER_DROPDOWN_OPTIONS, (opt) => {
+  if (opt.crypto) return "Crypto";
+  if (opt.equity) return "Equities";
+  if (opt.forex) return "Forex";
+  return "Ungrouped";
+});
+
+export const SOURCE_PICKER_DROPDOWN_GROUPS: SourcePickerDropdownGroup[] =
+  Object.entries(groupedOpts)
+    .sort(([groupNameA], [groupNameB]) => groupNameA.localeCompare(groupNameB))
+    .map(([groupName, options]) => ({
+      name: groupName,
+      options: options,
+    }));

+ 26 - 0
apps/pyth-pro-perf-dash/src/state/ui-state.ts

@@ -0,0 +1,26 @@
+import { create } from "zustand";
+
+import { SOURCE_PICKER_DROPDOWN_OPTIONS } from "../constants";
+import type { DataPoint, PythProPerfDashState } from "../types";
+
+export const useUIStateStore = create<PythProPerfDashState>()((set) => ({
+  crypto: {},
+  equities: {},
+  forex: {},
+  setDataPoint(which, thing, dataPoint) {
+    set((prev) => {
+      const arr = (prev[which][thing] ?? []) as DataPoint[];
+      arr.push(dataPoint);
+
+      // @ts-expect-error - typescript gets super confused about the narrowing here,
+      // despite this being a safe operation
+      prev[which][thing] = arr;
+      return prev;
+    });
+  },
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  selectedSource: SOURCE_PICKER_DROPDOWN_OPTIONS[0]!,
+  setSelectedSource(selectedSource) {
+    set({ selectedSource });
+  },
+}));

+ 45 - 0
apps/pyth-pro-perf-dash/src/types/index.ts

@@ -0,0 +1,45 @@
+import type { ArrayValues } from "type-fest";
+
+export const SUPPORTED_CURRENCY_PAIRS = ["EURUSD"] as const;
+export const SUPPORTED_EQUITIES = ["TSLA"] as const;
+export const SUPPORTED_CRYPTO_CURRENCIES = ["BTC"] as const;
+
+export type SupportedCurrencyPairs = ArrayValues<
+  typeof SUPPORTED_CURRENCY_PAIRS
+>;
+export type SupportedEquities = ArrayValues<typeof SUPPORTED_EQUITIES>;
+export type SupportedCryptoCurrencies = ArrayValues<
+  typeof SUPPORTED_CRYPTO_CURRENCIES
+>;
+
+export type DataPoint = {
+  timestamp: number;
+  value: number;
+};
+
+export type SourcePickerDropdownOption = {
+  id: string;
+  crypto: boolean;
+  equity: boolean;
+  forex: boolean;
+};
+
+export type SourcePickerDropdownGroup = {
+  name: string;
+  options: SourcePickerDropdownOption[];
+};
+
+export type PythProPerfDashState = {
+  crypto: Partial<Record<SupportedCryptoCurrencies, DataPoint[]>>;
+  equities: Partial<Record<SupportedEquities, DataPoint[]>>;
+  forex: Partial<Record<SupportedCurrencyPairs, DataPoint[]>>;
+  setDataPoint: <K extends StateKeys, K2 extends keyof PythProPerfDashState[K]>(
+    which: K,
+    thing: K2,
+    dataPoint: DataPoint,
+  ) => void;
+  selectedSource: SourcePickerDropdownOption;
+  setSelectedSource: (opt: SourcePickerDropdownOption) => void;
+};
+
+export type StateKeys = keyof Omit<PythProPerfDashState, "setDataPoint">;

+ 6 - 0
apps/pyth-pro-perf-dash/svg.d.ts

@@ -0,0 +1,6 @@
+declare module "*.svg" {
+  import type { ReactElement, SVGProps } from "react";
+
+  const content: (props: SVGProps<SVGElement>) => ReactElement;
+  export default content;
+}

+ 5 - 0
apps/pyth-pro-perf-dash/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "extends": "@cprussin/tsconfig/nextjs.json",
+  "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
+  "exclude": ["node_modules"]
+}

+ 36 - 0
apps/pyth-pro-perf-dash/turbo.json

@@ -0,0 +1,36 @@
+{
+  "$schema": "https://turbo.build/schema.json",
+  "extends": ["//"],
+  "tasks": {
+    "build:vercel": {
+      "env": []
+    },
+    "fix:lint": {
+      "dependsOn": [
+        "//#install:modules",
+        "fix:lint:eslint",
+        "fix:lint:stylelint"
+      ]
+    },
+    "fix:lint:eslint": {
+      "dependsOn": ["//#install:modules", "^build"],
+      "cache": false
+    },
+    "fix:lint:stylelint": {
+      "dependsOn": ["//#install:modules"],
+      "cache": false
+    },
+    "start:prod": {
+      "dependsOn": ["//#install:modules", "build:vercel"]
+    },
+    "test:lint": {
+      "dependsOn": ["test:lint:eslint", "test:lint:stylelint"]
+    },
+    "test:lint:eslint": {
+      "dependsOn": ["//#install:modules", "^build"]
+    },
+    "test:lint:stylelint": {
+      "dependsOn": ["//#install:modules"]
+    }
+  }
+}

+ 4 - 0
apps/pyth-pro-perf-dash/vercel.json

@@ -0,0 +1,4 @@
+{
+  "$schema": "https://openapi.vercel.sh/vercel.json",
+  "buildCommand": "turbo run build:vercel --filter @pythnetwork/pyth-pro-perf-dash"
+}

+ 131 - 0
pnpm-lock.yaml

@@ -1185,6 +1185,106 @@ importers:
         specifier: 'catalog:'
         version: 10.9.2(@swc/core@1.15.0)(@types/node@22.14.0)(typescript@5.9.3)
 
+  apps/pyth-pro-perf-dash:
+    dependencies:
+      '@next/third-parties':
+        specifier: 'catalog:'
+        version: 15.3.2(next@15.5.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0)
+      '@pythnetwork/component-library':
+        specifier: workspace:*
+        version: link:../../packages/component-library
+      '@pythnetwork/react-hooks':
+        specifier: workspace:*
+        version: link:../../packages/react-hooks
+      '@react-hookz/web':
+        specifier: 'catalog:'
+        version: 25.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      clsx:
+        specifier: 'catalog:'
+        version: 2.1.1
+      framer-motion:
+        specifier: 'catalog:'
+        version: 12.9.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      next:
+        specifier: 'catalog:'
+        version: 15.5.0(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)
+      nuqs:
+        specifier: 'catalog:'
+        version: 2.4.1(next@15.5.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0)
+      pino:
+        specifier: 'catalog:'
+        version: 9.6.0
+      react:
+        specifier: 'catalog:'
+        version: 19.1.0
+      react-aria:
+        specifier: 'catalog:'
+        version: 3.42.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      react-aria-components:
+        specifier: 'catalog:'
+        version: 1.11.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      react-dom:
+        specifier: 'catalog:'
+        version: 19.1.0(react@19.1.0)
+      swr:
+        specifier: 'catalog:'
+        version: 2.3.3(react@19.1.0)
+      zod:
+        specifier: 'catalog:'
+        version: 3.24.4
+      zustand:
+        specifier: ^5.0.8
+        version: 5.0.8(@types/react@19.1.0)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
+    devDependencies:
+      '@axe-core/react':
+        specifier: 'catalog:'
+        version: 4.10.1
+      '@cprussin/eslint-config':
+        specifier: 'catalog:'
+        version: 4.0.2(@testing-library/dom@10.4.1)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.23.0(jiti@2.4.2))(typescript@5.9.3))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.23.0(jiti@2.4.2))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.15.0)(@types/node@22.14.0)(typescript@5.9.3)))(ts-node@10.9.2(@swc/core@1.15.0)(@types/node@22.14.0)(typescript@5.9.3))(turbo@2.5.8)(typescript@5.9.3)
+      '@cprussin/prettier-config':
+        specifier: 'catalog:'
+        version: 2.2.2(prettier@3.5.3)
+      '@cprussin/tsconfig':
+        specifier: 'catalog:'
+        version: 3.1.2(typescript@5.9.3)
+      '@pythnetwork/jest-config':
+        specifier: 'workspace:'
+        version: link:../../packages/jest-config
+      '@types/jest':
+        specifier: 'catalog:'
+        version: 29.5.14
+      '@types/node':
+        specifier: 'catalog:'
+        version: 22.14.0
+      '@types/react':
+        specifier: 'catalog:'
+        version: 19.1.0
+      '@types/react-dom':
+        specifier: 'catalog:'
+        version: 19.1.1(@types/react@19.1.0)
+      autoprefixer:
+        specifier: 'catalog:'
+        version: 10.4.21(postcss@8.5.3)
+      eslint:
+        specifier: 'catalog:'
+        version: 9.23.0(jiti@2.4.2)
+      jest:
+        specifier: 'catalog:'
+        version: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.15.0)(@types/node@22.14.0)(typescript@5.9.3))
+      postcss:
+        specifier: 'catalog:'
+        version: 8.5.3
+      prettier:
+        specifier: 'catalog:'
+        version: 3.5.3
+      type-fest:
+        specifier: 'catalog:'
+        version: 5.2.0
+      vercel:
+        specifier: 'catalog:'
+        version: 41.4.1(@swc/core@1.15.0)(encoding@0.1.13)(rollup@4.52.5)
+
   apps/staking:
     dependencies:
       '@amplitude/analytics-browser':
@@ -22167,6 +22267,24 @@ packages:
       use-sync-external-store:
         optional: true
 
+  zustand@5.0.8:
+    resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
+    engines: {node: '>=12.20.0'}
+    peerDependencies:
+      '@types/react': '>=18.0.0'
+      immer: '>=9.0.6'
+      react: '>=18.0.0'
+      use-sync-external-store: '>=1.2.0'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      immer:
+        optional: true
+      react:
+        optional: true
+      use-sync-external-store:
+        optional: true
+
   zwitch@2.0.4:
     resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
 
@@ -29128,6 +29246,12 @@ snapshots:
       react: 19.1.0
       third-party-capital: 1.0.20
 
+  '@next/third-parties@15.3.2(next@15.5.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0)':
+    dependencies:
+      next: 15.5.0(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)
+      react: 19.1.0
+      third-party-capital: 1.0.20
+
   '@ngraveio/bc-ur@1.1.13':
     dependencies:
       '@keystonehq/alias-sampling': 0.1.2
@@ -52314,4 +52438,11 @@ snapshots:
       react: 19.1.0
       use-sync-external-store: 1.4.0(react@19.1.0)
 
+  zustand@5.0.8(@types/react@19.1.0)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)):
+    optionalDependencies:
+      '@types/react': 19.1.0
+      immer: 9.0.21
+      react: 19.1.0
+      use-sync-external-store: 1.5.0(react@19.1.0)
+
   zwitch@2.0.4: {}