Browse Source

feat(insights): initialize insights hub

Connor Prussin 1 year ago
parent
commit
28caaa0dc4
75 changed files with 1337 additions and 196 deletions
  1. 2 2
      .prettierignore
  2. 1 0
      apps/insights/.gitignore
  3. 7 0
      apps/insights/.prettierignore
  4. 9 0
      apps/insights/eslint.config.js
  5. 1 0
      apps/insights/jest.config.js
  6. 5 0
      apps/insights/next-env.d.ts
  7. 56 0
      apps/insights/next.config.js
  8. 48 0
      apps/insights/package.json
  9. 6 0
      apps/insights/postcss.config.js
  10. 9 0
      apps/insights/prettier.config.js
  11. BIN
      apps/insights/public/android-chrome-192x192.png
  12. BIN
      apps/insights/public/android-chrome-512x512.png
  13. BIN
      apps/insights/public/apple-touch-icon.png
  14. BIN
      apps/insights/public/favicon-16x16.png
  15. BIN
      apps/insights/public/favicon-32x32.png
  16. BIN
      apps/insights/public/favicon-light.ico
  17. BIN
      apps/insights/public/favicon.ico
  18. 3 0
      apps/insights/src/app/error.ts
  19. 14 0
      apps/insights/src/app/global-error.tsx
  20. 4 0
      apps/insights/src/app/layout.ts
  21. 25 0
      apps/insights/src/app/manifest.ts
  22. 1 0
      apps/insights/src/app/not-found.ts
  23. 1 0
      apps/insights/src/app/page.ts
  24. 11 0
      apps/insights/src/app/robots.ts
  25. 27 0
      apps/insights/src/components/Error/index.tsx
  26. 5 0
      apps/insights/src/components/Home/index.tsx
  27. 9 0
      apps/insights/src/components/NotFound/index.tsx
  28. 31 0
      apps/insights/src/components/Root/index.tsx
  29. 13 0
      apps/insights/src/config/isomorphic.ts
  30. 45 0
      apps/insights/src/config/server.ts
  31. 52 0
      apps/insights/src/metadata.ts
  32. 3 0
      apps/insights/src/tailwind.css
  33. 6 0
      apps/insights/svg.d.ts
  34. 10 0
      apps/insights/tailwind.config.ts
  35. 5 0
      apps/insights/tsconfig.json
  36. 20 0
      apps/insights/turbo.json
  37. 3 0
      apps/insights/vercel.json
  38. 2 0
      packages/app-logger/.prettierignore
  39. 1 0
      packages/app-logger/README.md
  40. 1 0
      packages/app-logger/eslint.config.js
  41. 1 0
      packages/app-logger/jest.config.js
  42. 36 0
      packages/app-logger/package.json
  43. 1 0
      packages/app-logger/prettier.config.js
  44. 8 0
      packages/app-logger/src/context.ts
  45. 17 0
      packages/app-logger/src/index.tsx
  46. 25 0
      packages/app-logger/src/provider.tsx
  47. 3 0
      packages/app-logger/tsconfig.json
  48. 32 27
      packages/component-library/package.json
  49. 12 7
      packages/component-library/src/Button/index.tsx
  50. 9 0
      packages/component-library/src/Button/react-aria-buttons.tsx
  51. 5 7
      packages/component-library/src/Checkbox/index.tsx
  52. 6 4
      packages/component-library/src/CheckboxGroup/index.tsx
  53. 2 6
      packages/component-library/src/Link/index.tsx
  54. 2 7
      packages/component-library/src/Radio/index.tsx
  55. 6 4
      packages/component-library/src/RadioGroup/index.tsx
  56. 4 0
      packages/component-library/src/index.ts
  57. 0 12
      packages/component-library/src/type-utils.ts
  58. 3 1
      packages/component-library/tailwind.config.ts
  59. 6 6
      packages/fonts/package.json
  60. 2 0
      packages/next-root/.prettierignore
  61. 1 0
      packages/next-root/README.md
  62. 1 0
      packages/next-root/eslint.config.js
  63. 1 0
      packages/next-root/jest.config.js
  64. 47 0
      packages/next-root/package.json
  65. 1 0
      packages/next-root/prettier.config.js
  66. 30 0
      packages/next-root/src/amplitude.tsx
  67. 17 0
      packages/next-root/src/compose-providers.tsx
  68. 11 0
      packages/next-root/src/html-with-lang.tsx
  69. 30 0
      packages/next-root/src/i18n-provider.tsx
  70. 47 0
      packages/next-root/src/index.tsx
  71. 27 0
      packages/next-root/src/report-accessibility.tsx
  72. 27 0
      packages/next-root/src/router-provider.tsx
  73. 3 0
      packages/next-root/tsconfig.json
  74. 441 110
      pnpm-lock.yaml
  75. 37 3
      pnpm-workspace.yaml

+ 2 - 2
.prettierignore

@@ -19,6 +19,6 @@ patches/
 # build graph config.
 apps/api-reference
 apps/staking
+apps/insights
 governance/pyth_staking_sdk
-packages/component-library
-packages/fonts
+packages/*

+ 1 - 0
apps/insights/.gitignore

@@ -0,0 +1 @@
+.env*.local

+ 7 - 0
apps/insights/.prettierignore

@@ -0,0 +1,7 @@
+.next/
+coverage/
+node_modules/
+*.tsbuildinfo
+.env*.local
+.env
+.DS_Store

+ 9 - 0
apps/insights/eslint.config.js

@@ -0,0 +1,9 @@
+import { fileURLToPath } from "node:url";
+
+import { nextjs, tailwind, storybook } from "@cprussin/eslint-config";
+
+const tailwindConfig = fileURLToPath(
+  import.meta.resolve(`./tailwind.config.ts`),
+);
+
+export default [...nextjs, ...tailwind(tailwindConfig), ...storybook];

+ 1 - 0
apps/insights/jest.config.js

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

+ 5 - 0
apps/insights/next-env.d.ts

@@ -0,0 +1,5 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

+ 56 - 0
apps/insights/next.config.js

@@ -0,0 +1,56 @@
+const config = {
+  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;
+  },
+
+  transpilePackages: ["@pythnetwork/*"],
+
+  headers: async () => [
+    {
+      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",
+        },
+      ],
+    },
+  ],
+};
+export default config;

+ 48 - 0
apps/insights/package.json

@@ -0,0 +1,48 @@
+{
+  "name": "@pythnetwork/insights",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "engines": {
+    "node": "20"
+  },
+  "scripts": {
+    "build": "next build",
+    "fix:format": "prettier --write .",
+    "fix:lint": "eslint --fix .",
+    "start:dev": "next dev --port 3003",
+    "start:prod": "next start --port 3003",
+    "test:format": "prettier --check .",
+    "test:lint": "jest --selectProjects lint",
+    "test:types": "tsc"
+  },
+  "dependencies": {
+    "@pythnetwork/app-logger": "workspace:*",
+    "@pythnetwork/component-library": "workspace:*",
+    "@pythnetwork/fonts": "workspace:*",
+    "@pythnetwork/next-root": "workspace:*",
+    "clsx": "catalog:",
+    "next": "catalog:",
+    "react": "catalog:",
+    "react-dom": "catalog:"
+  },
+  "devDependencies": {
+    "@cprussin/eslint-config": "catalog:",
+    "@cprussin/jest-config": "catalog:",
+    "@cprussin/prettier-config": "catalog:",
+    "@cprussin/tsconfig": "catalog:",
+    "@svgr/webpack": "catalog:",
+    "@types/jest": "catalog:",
+    "@types/node": "catalog:",
+    "@types/react": "catalog:",
+    "@types/react-dom": "catalog:",
+    "autoprefixer": "catalog:",
+    "eslint": "catalog:",
+    "jest": "catalog:",
+    "postcss": "catalog:",
+    "prettier": "catalog:",
+    "tailwindcss": "catalog:",
+    "typescript": "catalog:",
+    "vercel": "catalog:"
+  }
+}

+ 6 - 0
apps/insights/postcss.config.js

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

+ 9 - 0
apps/insights/prettier.config.js

@@ -0,0 +1,9 @@
+import { fileURLToPath } from "node:url";
+
+import { base, tailwind, mergeConfigs } from "@cprussin/prettier-config";
+
+const tailwindConfig = fileURLToPath(
+  import.meta.resolve(`./tailwind.config.ts`),
+);
+
+export default mergeConfigs([base, tailwind(tailwindConfig)]);

BIN
apps/insights/public/android-chrome-192x192.png


BIN
apps/insights/public/android-chrome-512x512.png


BIN
apps/insights/public/apple-touch-icon.png


BIN
apps/insights/public/favicon-16x16.png


BIN
apps/insights/public/favicon-32x32.png


BIN
apps/insights/public/favicon-light.ico


BIN
apps/insights/public/favicon.ico


+ 3 - 0
apps/insights/src/app/error.ts

@@ -0,0 +1,3 @@
+"use client";
+
+export { Error as default } from "../components/Error";

+ 14 - 0
apps/insights/src/app/global-error.tsx

@@ -0,0 +1,14 @@
+"use client";
+
+import type { ComponentProps } from "react";
+
+import { Error } from "../components/Error";
+
+const GlobalError = (props: ComponentProps<typeof Error>) => (
+  <html lang="en" dir="ltr">
+    <body>
+      <Error {...props} />
+    </body>
+  </html>
+);
+export default GlobalError;

+ 4 - 0
apps/insights/src/app/layout.ts

@@ -0,0 +1,4 @@
+import "../tailwind.css";
+
+export { Root as default } from "../components/Root";
+export { metadata, viewport } from "../metadata";

+ 25 - 0
apps/insights/src/app/manifest.ts

@@ -0,0 +1,25 @@
+import type { MetadataRoute } from "next";
+
+import { metadata, viewport } from "../metadata";
+
+const manifest = (): MetadataRoute.Manifest => ({
+  name: metadata.applicationName,
+  short_name: metadata.applicationName,
+  description: metadata.description,
+  theme_color: viewport.themeColor,
+  background_color: viewport.themeColor,
+  icons: [
+    {
+      src: "/android-chrome-192x192.png",
+      sizes: "192x192",
+      type: "image/png",
+    },
+    {
+      src: "/android-chrome-512x512.png",
+      sizes: "512x512",
+      type: "image/png",
+    },
+  ],
+  display: "standalone",
+});
+export default manifest;

+ 1 - 0
apps/insights/src/app/not-found.ts

@@ -0,0 +1 @@
+export { NotFound as default } from "../components/NotFound";

+ 1 - 0
apps/insights/src/app/page.ts

@@ -0,0 +1 @@
+export { Home as default } from "../components/Home";

+ 11 - 0
apps/insights/src/app/robots.ts

@@ -0,0 +1,11 @@
+import type { MetadataRoute } from "next";
+
+import { IS_PRODUCTION_SERVER } from "../config/server";
+
+const robots = (): MetadataRoute.Robots => ({
+  rules: {
+    userAgent: "*",
+    ...(IS_PRODUCTION_SERVER ? { allow: "/" } : { disallow: "/" }),
+  },
+});
+export default robots;

+ 27 - 0
apps/insights/src/components/Error/index.tsx

@@ -0,0 +1,27 @@
+import { useLogger } from "@pythnetwork/app-logger";
+import { Button } from "@pythnetwork/component-library/Button";
+import { useEffect } from "react";
+
+type Props = {
+  error: Error & { digest?: string };
+  reset?: () => void;
+};
+
+export const Error = ({ error, reset }: Props) => {
+  const logger = useLogger();
+
+  useEffect(() => {
+    logger.error(error);
+  }, [error, logger]);
+
+  return (
+    <main>
+      <h1>Uh oh!</h1>
+      <h2>Something went wrong</h2>
+      <p>
+        Error Details: <strong>{error.digest ?? error.message}</strong>
+      </p>
+      {reset && <Button onPress={reset}>Reset</Button>}
+    </main>
+  );
+};

+ 5 - 0
apps/insights/src/components/Home/index.tsx

@@ -0,0 +1,5 @@
+export const Home = () => (
+  <main>
+    <h1>Hello world!</h1>
+  </main>
+);

+ 9 - 0
apps/insights/src/components/NotFound/index.tsx

@@ -0,0 +1,9 @@
+import { ButtonLink } from "@pythnetwork/component-library/Button";
+
+export const NotFound = () => (
+  <main>
+    <h1>Not Found</h1>
+    <p>{"The page you're looking for isn't here"}</p>
+    <ButtonLink href="/">Go Home</ButtonLink>
+  </main>
+);

+ 31 - 0
apps/insights/src/components/Root/index.tsx

@@ -0,0 +1,31 @@
+import { sans } from "@pythnetwork/fonts";
+import { Root as BaseRoot } from "@pythnetwork/next-root";
+import clsx from "clsx";
+import type { ReactNode } from "react";
+
+import {
+  IS_PRODUCTION_SERVER,
+  GOOGLE_ANALYTICS_ID,
+  AMPLITUDE_API_KEY,
+} from "../../config/server";
+
+type Props = {
+  children: ReactNode;
+};
+
+export const Root = ({ children }: Props) => (
+  <BaseRoot
+    amplitudeApiKey={AMPLITUDE_API_KEY}
+    googleAnalyticsId={GOOGLE_ANALYTICS_ID}
+    enableAccessibilityReporting={!IS_PRODUCTION_SERVER}
+  >
+    <body
+      className={clsx(
+        "bg-white font-sans text-steel-900 antialiased dark:bg-steel-900 dark:text-steel-50",
+        sans.variable,
+      )}
+    >
+      {children}
+    </body>
+  </BaseRoot>
+);

+ 13 - 0
apps/insights/src/config/isomorphic.ts

@@ -0,0 +1,13 @@
+/* eslint-disable n/no-process-env */
+
+/**
+ * Indicates this is a production-optimized build.  Note this does NOT
+ * necessarily indicate that we're running on a cloud machine or the live build
+ * -- use `RUNNING_IN_CLOUD` or `IS_PRODUCTION_SERVER` out of `config/server.ts`
+ * for that (if you need that on the client you'll need to write a client
+ * component that receives that value as a prop).
+ *
+ * Basically this indicates if we're minified, excluding source maps, running
+ * with the optimized React build, etc.
+ */
+export const IS_PRODUCTION_BUILD = process.env.NODE_ENV === "production";

+ 45 - 0
apps/insights/src/config/server.ts

@@ -0,0 +1,45 @@
+// Disable the following rule because this file is the intended place to declare
+// and load all env variables.
+/* eslint-disable n/no-process-env */
+
+// Disable the following rule because variables in this file are only loaded at
+// runtime and do not influence the build outputs, thus they need not be
+// declared to turbo for it to be able to cache build outputs correctly.
+/* eslint-disable turbo/no-undeclared-env-vars */
+
+import "server-only";
+
+/**
+ * Throw if the env var `key` is not set (at either runtime or build time).
+ */
+const demand = (key: string): string => {
+  const value = process.env[key];
+  if (value === undefined || value === "") {
+    throw new MissingEnvironmentError(key);
+  } else {
+    return value;
+  }
+};
+
+/**
+ * Indicates that this server is the live customer-facing production server.
+ */
+export const IS_PRODUCTION_SERVER = process.env.VERCEL_ENV === "production";
+
+/**
+ * Throw if the env var `key` is not set in the live customer-facing production
+ * server, but allow it to be unset in any other environment.
+ */
+const demandInProduction = IS_PRODUCTION_SERVER
+  ? demand
+  : (key: string) => process.env[key];
+
+export const GOOGLE_ANALYTICS_ID = demandInProduction("GOOGLE_ANALYTICS_ID");
+export const AMPLITUDE_API_KEY = demandInProduction("AMPLITUDE_API_KEY");
+
+class MissingEnvironmentError extends Error {
+  constructor(name: string) {
+    super(`Missing environment variable: ${name}!`);
+    this.name = "MissingEnvironmentError";
+  }
+}

+ 52 - 0
apps/insights/src/metadata.ts

@@ -0,0 +1,52 @@
+import type { Metadata, Viewport } from "next";
+
+export const metadata = {
+  metadataBase: new URL("https://insights.pyth.network"),
+  title: {
+    default: "Pyth Network Insights",
+    template: "%s | Pyth Network Insights",
+  },
+  applicationName: "Pyth Network Insights",
+  description:
+    "Learn more about the Pyth network and explore data about the network's publishers and price feeds.",
+  referrer: "strict-origin-when-cross-origin",
+  openGraph: {
+    type: "website",
+  },
+  twitter: {
+    creator: "@PythNetwork",
+    card: "summary_large_image",
+  },
+  icons: {
+    icon: [
+      {
+        media: "(prefers-color-scheme: light)",
+        type: "image/x-icon",
+        url: "/favicon.ico",
+      },
+      {
+        media: "(prefers-color-scheme: dark)",
+        type: "image/x-icon",
+        url: "/favicon-light.ico",
+      },
+      {
+        type: "image/png",
+        sizes: "32x32",
+        url: "/favicon-32x32.png",
+      },
+      {
+        type: "image/png",
+        sizes: "16x16",
+        url: "/favicon-16x16.png",
+      },
+    ],
+    apple: {
+      url: "/apple-touch-icon.png",
+      sizes: "180x180",
+    },
+  },
+} satisfies Metadata;
+
+export const viewport = {
+  themeColor: "#242235",
+} satisfies Viewport;

+ 3 - 0
apps/insights/src/tailwind.css

@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;

+ 6 - 0
apps/insights/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;
+}

+ 10 - 0
apps/insights/tailwind.config.ts

@@ -0,0 +1,10 @@
+import { tailwindGlob } from "@pythnetwork/component-library";
+import componentLibraryConfig from "@pythnetwork/component-library/tailwind-config";
+import type { Config } from "tailwindcss";
+
+const tailwindConfig = {
+  content: [tailwindGlob, "src/components/**/*.{ts,tsx}"],
+  presets: [componentLibraryConfig],
+} satisfies Config;
+
+export default tailwindConfig;

+ 5 - 0
apps/insights/tsconfig.json

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

+ 20 - 0
apps/insights/turbo.json

@@ -0,0 +1,20 @@
+{
+  "$schema": "https://turbo.build/schema.json",
+  "extends": ["//"],
+  "tasks": {
+    "build": {
+      "dependsOn": ["^build"],
+      "outputs": [".next/**", "!.next/cache/**"],
+      "env": ["GOOGLE_ANALYTICS_ID", "AMPLITUDE_API_KEY"]
+    },
+    "start:dev": {
+      "persistent": true,
+      "cache": false
+    },
+    "start:prod": {
+      "dependsOn": ["build"],
+      "persistent": true,
+      "cache": false
+    }
+  }
+}

+ 3 - 0
apps/insights/vercel.json

@@ -0,0 +1,3 @@
+{
+  "ignoreCommand": "pnpm dlx turbo-ignore --fallback=HEAD^"
+}

+ 2 - 0
packages/app-logger/.prettierignore

@@ -0,0 +1,2 @@
+coverage/
+node_modules/

+ 1 - 0
packages/app-logger/README.md

@@ -0,0 +1 @@
+# @pythnetwork/app-logger

+ 1 - 0
packages/app-logger/eslint.config.js

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

+ 1 - 0
packages/app-logger/jest.config.js

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

+ 36 - 0
packages/app-logger/package.json

@@ -0,0 +1,36 @@
+{
+  "name": "@pythnetwork/app-logger",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "exports": {
+    ".": "./src/index.tsx",
+    "./provider": "./src/provider.tsx"
+  },
+  "scripts": {
+    "fix:format": "prettier --write .",
+    "fix:lint": "eslint --fix .",
+    "test:format": "jest --selectProjects format",
+    "test:lint": "jest --selectProjects lint",
+    "test:types": "tsc"
+  },
+  "peerDependencies": {
+    "react": "catalog:"
+  },
+  "dependencies": {
+    "pino": "catalog:"
+  },
+  "devDependencies": {
+    "@cprussin/eslint-config": "catalog:",
+    "@cprussin/jest-config": "catalog:",
+    "@cprussin/prettier-config": "catalog:",
+    "@cprussin/tsconfig": "catalog:",
+    "@types/jest": "catalog:",
+    "@types/react": "catalog:",
+    "eslint": "catalog:",
+    "jest": "catalog:",
+    "prettier": "catalog:",
+    "react": "catalog:",
+    "typescript": "catalog:"
+  }
+}

+ 1 - 0
packages/app-logger/prettier.config.js

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

+ 8 - 0
packages/app-logger/src/context.ts

@@ -0,0 +1,8 @@
+"use client";
+
+import type { Logger } from "pino";
+import { createContext } from "react";
+
+export const LoggerContext = createContext<undefined | Logger<string>>(
+  undefined,
+);

+ 17 - 0
packages/app-logger/src/index.tsx

@@ -0,0 +1,17 @@
+import { useContext } from "react";
+
+import { LoggerContext } from "./context.js";
+
+export const useLogger = () => {
+  const logger = useContext(LoggerContext);
+  if (logger) {
+    return logger;
+  } else {
+    throw new NotInitializedError();
+  }
+};
+
+class NotInitializedError extends Error {
+  override message =
+    "This component must be contained within a `LoggerProvider`!";
+}

+ 25 - 0
packages/app-logger/src/provider.tsx

@@ -0,0 +1,25 @@
+"use client";
+
+import { pino } from "pino";
+import { type ComponentProps, useMemo } from "react";
+
+import { LoggerContext } from "./context.js";
+
+type LoggerProviderProps = Omit<
+  ComponentProps<typeof LoggerContext.Provider>,
+  "config" | "value"
+> & {
+  config?: Parameters<typeof pino>[0] | undefined;
+};
+
+export const LoggerProvider = ({ config, ...props }: LoggerProviderProps) => {
+  const logger = useMemo(
+    () =>
+      pino({
+        ...config,
+        browser: { ...config?.browser },
+      }),
+    [config],
+  );
+  return <LoggerContext.Provider value={logger} {...props} />;
+};

+ 3 - 0
packages/app-logger/tsconfig.json

@@ -0,0 +1,3 @@
+{
+  "extends": "@cprussin/tsconfig/react.json"
+}

+ 32 - 27
packages/component-library/package.json

@@ -3,6 +3,11 @@
   "version": "0.0.0",
   "private": true,
   "type": "module",
+  "exports": {
+    ".": "./src/index.ts",
+    "./tailwind-config": "./tailwind.config.ts",
+    "./*": "./src/*/index.tsx"
+  },
   "scripts": {
     "build:storybook": "storybook build",
     "fix:format": "prettier --write .",
@@ -16,39 +21,39 @@
     "react": "catalog:"
   },
   "dependencies": {
-    "clsx": "^2.1.1",
-    "react-aria": "^3.35.0",
-    "react-aria-components": "^1.4.0"
+    "clsx": "catalog:",
+    "react-aria": "catalog:",
+    "react-aria-components": "catalog:"
   },
   "devDependencies": {
     "@cprussin/eslint-config": "catalog:",
     "@cprussin/jest-config": "catalog:",
     "@cprussin/prettier-config": "catalog:",
     "@cprussin/tsconfig": "catalog:",
-    "@phosphor-icons/react": "^2.1.7",
-    "@pythnetwork/fonts": "workspace:^",
-    "@storybook/addon-essentials": "^8.3.5",
-    "@storybook/addon-styling-webpack": "^1.0.0",
-    "@storybook/addon-themes": "^8.3.5",
-    "@storybook/blocks": "^8.3.5",
-    "@storybook/nextjs": "^8.3.5",
-    "@storybook/react": "^8.3.5",
-    "@tailwindcss/forms": "^0.5.9",
-    "@types/jest": "^29.5.13",
+    "@phosphor-icons/react": "catalog:",
+    "@pythnetwork/fonts": "workspace:*",
+    "@storybook/addon-essentials": "catalog:",
+    "@storybook/addon-styling-webpack": "catalog:",
+    "@storybook/addon-themes": "catalog:",
+    "@storybook/blocks": "catalog:",
+    "@storybook/nextjs": "catalog:",
+    "@storybook/react": "catalog:",
+    "@tailwindcss/forms": "catalog:",
+    "@types/jest": "catalog:",
     "@types/react": "catalog:",
-    "autoprefixer": "^10.4.20",
-    "css-loader": "^7.1.2",
-    "eslint": "^9.12.0",
-    "jest": "^29.7.0",
-    "postcss": "^8.4.47",
-    "postcss-loader": "^8.1.1",
-    "prettier": "^3.3.3",
-    "react": "^18.3.1",
-    "storybook": "^8.3.5",
-    "style-loader": "^4.0.0",
-    "tailwindcss": "^3.4.13",
-    "tailwindcss-animate": "^1.0.7",
-    "tailwindcss-react-aria-components": "^1.1.6",
-    "typescript": "^5.6.3"
+    "autoprefixer": "catalog:",
+    "css-loader": "catalog:",
+    "eslint": "catalog:",
+    "jest": "catalog:",
+    "postcss": "catalog:",
+    "postcss-loader": "catalog:",
+    "prettier": "catalog:",
+    "react": "catalog:",
+    "storybook": "catalog:",
+    "style-loader": "catalog:",
+    "tailwindcss": "catalog:",
+    "tailwindcss-animate": "catalog:",
+    "tailwindcss-react-aria-components": "catalog:",
+    "typescript": "catalog:"
   }
 }

+ 12 - 7
packages/component-library/src/Button/index.tsx

@@ -1,8 +1,11 @@
 import clsx from "clsx";
-import type { ComponentType, ReactNode } from "react";
-import { Button as BaseButton, Link } from "react-aria-components";
+import type { ComponentType, ReactNode, SVGProps } from "react";
+import {
+  type ButtonProps as BaseButtonProps,
+  type LinkProps as BaseLinkProps,
+} from "react-aria-components";
 
-import type { Icon, ExtendComponentProps } from "../type-utils.js";
+import { Button as BaseButton, Link } from "./react-aria-buttons.js";
 
 export const VARIANTS = [
   "primary",
@@ -24,7 +27,7 @@ type OwnProps = {
   afterIcon?: Icon | undefined;
 };
 
-export type ButtonProps = ExtendComponentProps<typeof BaseButton, OwnProps>;
+export type ButtonProps = Omit<BaseButtonProps, keyof OwnProps> & OwnProps;
 
 export const Button = ({ className, ...props }: ButtonProps) => (
   <ButtonImpl
@@ -39,7 +42,7 @@ export const Button = ({ className, ...props }: ButtonProps) => (
   />
 );
 
-export type ButtonLinkProps = ExtendComponentProps<typeof Link, OwnProps>;
+export type ButtonLinkProps = Omit<BaseLinkProps, keyof OwnProps> & OwnProps;
 
 export const ButtonLink = (props: ButtonLinkProps) => (
   <ButtonImpl component={Link} {...props} />
@@ -103,10 +106,10 @@ const baseClasses = clsx(
   "data-[size=lg]:h-14 data-[size=lg]:rounded-2xl data-[size=lg]:px-4 data-[size=lg]:text-xl data-[size=lg]:leading-[3.5rem]",
 
   // Primary (default)
-  "data-[variant=primary]:bg-violet-500 data-[variant=primary]:data-[hovered]:bg-violet-700 data-[variant=primary]:data-[pressed]:bg-violet-800 data-[variant=primary]:text-violet-50 data-[variant=primary]:outline-violet-500",
+  "data-[variant=primary]:bg-violet-700 data-[variant=primary]:data-[hovered]:bg-violet-800 data-[variant=primary]:data-[pressed]:bg-violet-900 data-[variant=primary]:text-white data-[variant=primary]:outline-violet-700",
 
   // Dark Mode Primary (default)
-  "dark:data-[variant=primary]:bg-violet-600 dark:data-[variant=primary]:data-[hovered]:bg-violet-700 dark:data-[variant=primary]:data-[pressed]:bg-violet-800 dark:data-[variant=primary]:text-violet-50 dark:data-[variant=primary]:outline-violet-600",
+  "dark:data-[variant=primary]:bg-violet-600 dark:data-[variant=primary]:data-[hovered]:bg-violet-700 dark:data-[variant=primary]:data-[pressed]:bg-violet-800 dark:data-[variant=primary]:text-white dark:data-[variant=primary]:outline-violet-600",
 
   // Secondary
   "data-[variant=secondary]:bg-purple-200 data-[variant=secondary]:data-[hovered]:bg-purple-300 data-[variant=secondary]:data-[pressed]:bg-purple-400 data-[variant=secondary]:text-steel-900 data-[variant=secondary]:outline-purple-300",
@@ -147,3 +150,5 @@ const baseClasses = clsx(
   // Disabled
   "data-[disabled]:data-[variant]:cursor-not-allowed data-[disabled]:data-[variant]:border-transparent data-[disabled]:data-[variant]:bg-stone-200 data-[disabled]:data-[variant]:text-stone-400 dark:data-[disabled]:data-[variant]:bg-steel-600 dark:data-[disabled]:data-[variant]:text-steel-400",
 );
+
+type Icon = ComponentType<SVGProps<SVGSVGElement>>;

+ 9 - 0
packages/component-library/src/Button/react-aria-buttons.tsx

@@ -0,0 +1,9 @@
+/**
+ * The react-aria components aren't marked as "use client" so it's a bit
+ * obnoxious to use them; this file just adds a client boundary and re-exports
+ * the react-aria components to avoid that problem.
+ */
+
+"use client";
+
+export { Button, Link } from "react-aria-components";

+ 5 - 7
packages/component-library/src/Checkbox/index.tsx

@@ -1,12 +1,10 @@
 import clsx from "clsx";
-import type { ComponentProps } from "react";
-import { Checkbox as BaseCheckbox } from "react-aria-components";
+import {
+  Checkbox as BaseCheckbox,
+  type CheckboxProps,
+} from "react-aria-components";
 
-export const Checkbox = ({
-  children,
-  className,
-  ...props
-}: ComponentProps<typeof BaseCheckbox>) => (
+export const Checkbox = ({ children, className, ...props }: CheckboxProps) => (
   <BaseCheckbox
     className={clsx(
       "group/checkbox inline-flex cursor-pointer flex-row gap-2 py-1 text-sm data-[disabled]:cursor-not-allowed",

+ 6 - 4
packages/component-library/src/CheckboxGroup/index.tsx

@@ -1,6 +1,8 @@
 import clsx from "clsx";
-import type { ComponentProps } from "react";
 import {
+  type LabelProps,
+  type TextProps,
+  type CheckboxGroupProps as BaseCheckboxGroupProps,
   CheckboxGroup as BaseCheckboxGroup,
   Label,
   Text,
@@ -8,9 +10,9 @@ import {
 
 export const ORIENTATIONS = ["vertical", "horizontal"] as const;
 
-type CheckboxGroupProps = ComponentProps<typeof BaseCheckboxGroup> & {
-  label: ComponentProps<typeof Label>["children"];
-  description?: ComponentProps<typeof Text>["children"] | undefined;
+type CheckboxGroupProps = BaseCheckboxGroupProps & {
+  label: LabelProps["children"];
+  description?: TextProps["children"] | undefined;
   orientation?: (typeof ORIENTATIONS)[number] | undefined;
 };
 

+ 2 - 6
packages/component-library/src/Link/index.tsx

@@ -1,11 +1,7 @@
 import clsx from "clsx";
-import type { ComponentProps } from "react";
-import { Link as BaseLink } from "react-aria-components";
+import { Link as BaseLink, type LinkProps } from "react-aria-components";
 
-export const Link = ({
-  className,
-  ...props
-}: ComponentProps<typeof BaseLink>) => (
+export const Link = ({ className, ...props }: LinkProps) => (
   <BaseLink
     className={clsx(
       "underline outline-0 outline-offset-4 outline-inherit data-[disabled]:cursor-not-allowed data-[disabled]:text-stone-400 data-[disabled]:no-underline data-[focus-visible]:outline-2 hover:no-underline dark:data-[disabled]:text-steel-400",

+ 2 - 7
packages/component-library/src/Radio/index.tsx

@@ -1,12 +1,7 @@
 import clsx from "clsx";
-import type { ComponentProps } from "react";
-import { Radio as BaseRadio } from "react-aria-components";
+import { Radio as BaseRadio, type RadioProps } from "react-aria-components";
 
-export const Radio = ({
-  children,
-  className,
-  ...props
-}: ComponentProps<typeof BaseRadio>) => (
+export const Radio = ({ children, className, ...props }: RadioProps) => (
   <BaseRadio
     className={clsx(
       "group/radio inline-flex cursor-pointer flex-row gap-2 py-1 text-sm data-[disabled]:cursor-not-allowed data-[selected]:cursor-default",

+ 6 - 4
packages/component-library/src/RadioGroup/index.tsx

@@ -1,14 +1,16 @@
 import clsx from "clsx";
-import type { ComponentProps } from "react";
 import {
+  type LabelProps,
+  type TextProps,
+  type RadioGroupProps as BaseRadioGroupProps,
   RadioGroup as BaseRadioGroup,
   Label,
   Text,
 } from "react-aria-components";
 
-type CheckboxGroupProps = ComponentProps<typeof BaseRadioGroup> & {
-  label: ComponentProps<typeof Label>["children"];
-  description?: ComponentProps<typeof Text>["children"] | undefined;
+type CheckboxGroupProps = BaseRadioGroupProps & {
+  label: LabelProps["children"];
+  description?: TextProps["children"] | undefined;
 };
 
 export const RadioGroup = ({

+ 4 - 0
packages/component-library/src/index.ts

@@ -0,0 +1,4 @@
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+export const tailwindGlob = `${path.dirname(fileURLToPath(import.meta.url))}/**/*.{ts,tsx}`;

+ 0 - 12
packages/component-library/src/type-utils.ts

@@ -1,12 +0,0 @@
-import type {
-  ComponentProps,
-  ComponentType,
-  SVGProps,
-  JSXElementConstructor,
-} from "react";
-
-export type Icon = ComponentType<SVGProps<SVGSVGElement>>;
-export type ExtendComponentProps<
-  Component extends JSXElementConstructor<object>,
-  NewProps,
-> = Omit<ComponentProps<Component>, keyof NewProps> & NewProps;

+ 3 - 1
packages/component-library/tailwind.config.ts

@@ -4,8 +4,10 @@ import plugin from "tailwindcss/plugin.js";
 import animate from "tailwindcss-animate";
 import reactAria from "tailwindcss-react-aria-components";
 
+import { tailwindGlob } from "./src/index.js";
+
 const tailwindConfig = {
-  content: ["src/**/*.tsx", ".storybook/**/*.tsx"],
+  content: [tailwindGlob, ".storybook/**/*.tsx"],
   darkMode: "class",
   theme: {
     extend: {

+ 6 - 6
packages/fonts/package.json

@@ -19,11 +19,11 @@
     "@cprussin/jest-config": "catalog:",
     "@cprussin/prettier-config": "catalog:",
     "@cprussin/tsconfig": "catalog:",
-    "@types/jest": "^29.5.13",
-    "eslint": "^9.12.0",
-    "jest": "^29.7.0",
-    "next": "^14.2.15",
-    "prettier": "^3.3.3",
-    "typescript": "^5.6.3"
+    "@types/jest": "catalog:",
+    "eslint": "catalog:",
+    "jest": "catalog:",
+    "next": "catalog:",
+    "prettier": "catalog:",
+    "typescript": "catalog:"
   }
 }

+ 2 - 0
packages/next-root/.prettierignore

@@ -0,0 +1,2 @@
+coverage/
+node_modules/

+ 1 - 0
packages/next-root/README.md

@@ -0,0 +1 @@
+# @pythnetwork/next-root

+ 1 - 0
packages/next-root/eslint.config.js

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

+ 1 - 0
packages/next-root/jest.config.js

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

+ 47 - 0
packages/next-root/package.json

@@ -0,0 +1,47 @@
+{
+  "name": "@pythnetwork/next-root",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "exports": {
+    ".": "./src/index.tsx"
+  },
+  "scripts": {
+    "fix:format": "prettier --write .",
+    "fix:lint": "eslint --fix .",
+    "test:format": "prettier --check .",
+    "test:lint": "jest --selectProjects lint",
+    "test:types": "tsc"
+  },
+  "peerDependencies": {
+    "next": "catalog:",
+    "react": "catalog:",
+    "react-aria": "catalog:",
+    "react-dom": "catalog:"
+  },
+  "dependencies": {
+    "@amplitude/analytics-browser": "catalog:",
+    "@amplitude/plugin-autocapture-browser": "catalog:",
+    "@axe-core/react": "catalog:",
+    "@next/third-parties": "catalog:",
+    "@pythnetwork/app-logger": "workspace:*",
+    "bcp-47": "catalog:"
+  },
+  "devDependencies": {
+    "@cprussin/eslint-config": "catalog:",
+    "@cprussin/jest-config": "catalog:",
+    "@cprussin/prettier-config": "catalog:",
+    "@cprussin/tsconfig": "catalog:",
+    "@types/jest": "catalog:",
+    "@types/react": "catalog:",
+    "@types/react-dom": "catalog:",
+    "eslint": "catalog:",
+    "jest": "catalog:",
+    "next": "catalog:",
+    "prettier": "catalog:",
+    "react": "catalog:",
+    "react-aria": "catalog:",
+    "react-dom": "catalog:",
+    "typescript": "catalog:"
+  }
+}

+ 1 - 0
packages/next-root/prettier.config.js

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

+ 30 - 0
packages/next-root/src/amplitude.tsx

@@ -0,0 +1,30 @@
+"use client";
+
+import * as amplitude from "@amplitude/analytics-browser";
+import { autocapturePlugin } from "@amplitude/plugin-autocapture-browser";
+import { useEffect, useRef } from "react";
+
+type Props = {
+  apiKey: string;
+};
+
+export const Amplitude = ({ apiKey }: Props) => {
+  useAmplitude(apiKey);
+
+  // eslint-disable-next-line unicorn/no-null
+  return null;
+};
+
+const useAmplitude = (apiKey: string) => {
+  const amplitudeInitialized = useRef(false);
+
+  useEffect(() => {
+    if (!amplitudeInitialized.current) {
+      amplitude.add(autocapturePlugin());
+      amplitude.init(apiKey, {
+        defaultTracking: true,
+      });
+      amplitudeInitialized.current = true;
+    }
+  }, [apiKey]);
+};

+ 17 - 0
packages/next-root/src/compose-providers.tsx

@@ -0,0 +1,17 @@
+import type { ComponentType, ReactNode } from "react";
+
+type ComposeProvidersProps = {
+  providers: ComponentType<{ children: ReactNode }>[];
+  children?: ReactNode | ReactNode[];
+};
+
+export const ComposeProviders = ({
+  providers,
+  children,
+}: ComposeProvidersProps) => {
+  let node = children;
+  for (const Provider of providers) {
+    node = <Provider>{node}</Provider>;
+  }
+  return node;
+};

+ 11 - 0
packages/next-root/src/html-with-lang.tsx

@@ -0,0 +1,11 @@
+"use client";
+
+import type { ComponentProps } from "react";
+import { useLocale } from "react-aria";
+
+type HtmlWithLangProps = Omit<ComponentProps<"html">, "lang">;
+
+export const HtmlWithLang = (props: HtmlWithLangProps) => {
+  const locale = useLocale();
+  return <html lang={locale.locale} dir={locale.direction} {...props} />;
+};

+ 30 - 0
packages/next-root/src/i18n-provider.tsx

@@ -0,0 +1,30 @@
+"use client";
+
+import { parse } from "bcp-47";
+import { useMemo, type ComponentProps } from "react";
+import { I18nProvider as I18nProviderBase, useIsSSR } from "react-aria";
+
+const SUPPORTED_LANGUAGES = new Set(["en"]);
+const DEFAULT_LOCALE = "en-US";
+
+export const I18nProvider = (
+  props: Omit<ComponentProps<typeof I18nProviderBase>, "locale">,
+) => {
+  const isSSR = useIsSSR();
+  const locale = useMemo(
+    () =>
+      isSSR
+        ? DEFAULT_LOCALE
+        : (window.navigator.languages.find((locale) => {
+            const language = parse(locale).language;
+            return (
+              language !== undefined &&
+              language !== null &&
+              SUPPORTED_LANGUAGES.has(language)
+            );
+          }) ?? DEFAULT_LOCALE),
+    [isSSR],
+  );
+
+  return <I18nProviderBase locale={locale} {...props} />;
+};

+ 47 - 0
packages/next-root/src/index.tsx

@@ -0,0 +1,47 @@
+import { GoogleAnalytics } from "@next/third-parties/google";
+import { LoggerProvider } from "@pythnetwork/app-logger/provider";
+import dynamic from "next/dynamic";
+import type { ComponentProps, ReactNode } from "react";
+
+import { Amplitude } from "./amplitude";
+import { ComposeProviders } from "./compose-providers";
+import { HtmlWithLang } from "./html-with-lang";
+import { I18nProvider } from "./i18n-provider";
+import { RouterProvider } from "./router-provider";
+
+const ReportAccessibility = dynamic(() =>
+  import("./report-accessibility.js").then((mod) => mod.ReportAccessibility),
+);
+
+type Props = Omit<ComponentProps<typeof HtmlWithLang>, "children"> & {
+  children: ReactNode;
+  enableAccessibilityReporting: boolean;
+  amplitudeApiKey?: string | undefined;
+  googleAnalyticsId?: string | undefined;
+  providers?: ComponentProps<typeof ComposeProviders>["providers"] | undefined;
+};
+
+export const Root = ({
+  children,
+  providers,
+  amplitudeApiKey,
+  googleAnalyticsId,
+  enableAccessibilityReporting,
+  ...props
+}: Props) => (
+  <ComposeProviders
+    providers={[
+      LoggerProvider,
+      I18nProvider,
+      RouterProvider,
+      ...(providers ?? []),
+    ]}
+  >
+    <HtmlWithLang {...props}>
+      {children}
+      {googleAnalyticsId && <GoogleAnalytics gaId={googleAnalyticsId} />}
+      {amplitudeApiKey && <Amplitude apiKey={amplitudeApiKey} />}
+      {enableAccessibilityReporting && <ReportAccessibility />}
+    </HtmlWithLang>
+  </ComposeProviders>
+);

+ 27 - 0
packages/next-root/src/report-accessibility.tsx

@@ -0,0 +1,27 @@
+"use client";
+
+import { useLogger } from "@pythnetwork/app-logger";
+import { useEffect } from "react";
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+
+const AXE_TIMEOUT = 1000;
+
+export const ReportAccessibility = () => {
+  useReportAccessibility();
+
+  // eslint-disable-next-line unicorn/no-null
+  return null;
+};
+
+const useReportAccessibility = () => {
+  const logger = useLogger();
+
+  useEffect(() => {
+    import("@axe-core/react")
+      .then((axe) => axe.default(React, ReactDOM, AXE_TIMEOUT))
+      .catch((error: unknown) => {
+        logger.error("Error setting up axe for accessibility testing", error);
+      });
+  }, [logger]);
+};

+ 27 - 0
packages/next-root/src/router-provider.tsx

@@ -0,0 +1,27 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { type ComponentProps, useCallback } from "react";
+import { RouterProvider as ReactAriaRouterProvider } from "react-aria";
+
+declare module "react-aria" {
+  type RouterConfig = {
+    routerOptions: NonNullable<
+      Parameters<ReturnType<typeof useRouter>["push"]>[1]
+    >;
+  };
+}
+
+export const RouterProvider = (
+  props: Omit<ComponentProps<typeof ReactAriaRouterProvider>, "navigate">,
+) => {
+  const router = useRouter();
+  const navigate = useCallback(
+    (...params: Parameters<typeof router.push>) => {
+      router.push(...params);
+    },
+    [router],
+  );
+
+  return <ReactAriaRouterProvider navigate={navigate} {...props} />;
+};

+ 3 - 0
packages/next-root/tsconfig.json

@@ -0,0 +1,3 @@
+{
+  "extends": "@cprussin/tsconfig/nextjs.json"
+}

File diff suppressed because it is too large
+ 441 - 110
pnpm-lock.yaml


+ 37 - 3
pnpm-workspace.yaml

@@ -31,13 +31,47 @@ packages:
   - "contract_manager"
 
 catalog:
+  "@amplitude/analytics-browser": 2.11.8
+  "@amplitude/plugin-autocapture-browser": 1.0.0
+  "@axe-core/react": 4.9.1
   "@cprussin/eslint-config": 3.0.0
   "@cprussin/jest-config": 1.4.1
   "@cprussin/prettier-config": 2.1.1
   "@cprussin/tsconfig": 3.0.1
-  "@types/react-dom": npm:types-react-dom@rc
-  "@types/react": npm:types-react@rc
+  "@next/third-parties": 15.0.2
+  "@phosphor-icons/react": 2.1.7
+  "@storybook/addon-essentials": 8.3.5
+  "@storybook/addon-styling-webpack": 1.0.0
+  "@storybook/addon-themes": 8.3.5
+  "@storybook/blocks": 8.3.5
+  "@storybook/nextjs": 8.3.5
+  "@storybook/react": 8.3.5
+  "@svgr/webpack": 8.1.0
+  "@tailwindcss/forms": 0.5.9
+  "@types/jest": 29.5.14
+  "@types/node": 22.8.2
+  "@types/react": npm:types-react@19.0.0-rc.1
+  "@types/react-dom": npm:types-react-dom@19.0.0-rc.1
+  autoprefixer: 10.4.20
+  bcp-47: 2.1.0
+  clsx: 2.1.1
+  css-loader: 7.1.2
+  eslint: 9.13.0
+  framer-motion: 11.11.10
+  jest: 29.7.0
   next: 15.0.2
+  pino: 9.5.0
+  postcss-loader: 8.1.1
+  postcss: 8.4.47
+  prettier: 3.3.3
+  react-aria-components: 1.4.0
+  react-aria: 3.35.0
   react-dom: 19.0.0-rc-603e6108-20241029
   react: 19.0.0-rc-603e6108-20241029
-  framer-motion: 11.11.10
+  storybook: 8.3.5
+  style-loader: 4.0.0
+  tailwindcss-animate: 1.0.7
+  tailwindcss-react-aria-components: 1.1.6
+  tailwindcss: 3.4.14
+  typescript: 5.6.3
+  vercel: 37.12.1

Some files were not shown because too many files changed in this diff