[
  {
    "path": ".eslintignore",
    "content": "# next config\nnext.config.js\n\n# tailwind config\ntailwind.config.js\npostcss.config.js\n\n# jest config\njest.config.js\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"project\": \"tsconfig.json\"\n  },\n  \"settings\": {\n    \"import/resolver\": {\n      \"typescript\": true,\n      \"node\": true\n    }\n  },\n  \"plugins\": [\"react-hooks\"]\n}\n"
  },
  {
    "path": ".eslintrc.json.bak",
    "content": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"project\": \"tsconfig.json\"\n  },\n  \"plugins\": [\"@typescript-eslint\"],\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:import/recommended\",\n    \"plugin:import/typescript\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:@typescript-eslint/recommended-requiring-type-checking\",\n    \"next/core-web-vitals\"\n  ],\n  \"settings\": {\n    \"import/resolver\": {\n      \"typescript\": true,\n      \"node\": true\n    }\n  },\n  \"rules\": {\n    \"semi\": [\"error\", \"always\"],\n    \"curly\": [\"warn\", \"multi\"],\n    \"quotes\": [\"error\", \"single\", { \"avoidEscape\": true }],\n    \"jsx-quotes\": [\"error\", \"prefer-single\"],\n    \"linebreak-style\": [\"error\", \"unix\"],\n    \"no-console\": \"warn\",\n    \"comma-dangle\": [\"error\", \"never\"],\n    \"no-unused-expressions\": \"error\",\n    \"no-constant-binary-expression\": \"error\",\n    \"import/order\": [\n      \"warn\",\n      {\n        \"pathGroups\": [\n          {\n            \"pattern\": \"*.scss\",\n            \"group\": \"builtin\",\n            \"position\": \"before\",\n            \"patternOptions\": { \"matchBase\": true }\n          },\n          {\n            \"pattern\": \"@lib/**\",\n            \"group\": \"external\",\n            \"position\": \"after\"\n          },\n          {\n            \"pattern\": \"@components/**\",\n            \"group\": \"external\",\n            \"position\": \"after\"\n          }\n        ],\n        \"warnOnUnassignedImports\": true,\n        \"pathGroupsExcludedImportTypes\": [\"type\"],\n        \"groups\": [\n          \"builtin\",\n          \"external\",\n          \"internal\",\n          \"parent\",\n          \"sibling\",\n          \"index\",\n          \"object\",\n          \"type\"\n        ]\n      }\n    ],\n    \"@typescript-eslint/no-misused-promises\": [\n      \"error\",\n      {\n        \"checksVoidReturn\": { \"attributes\": false }\n      }\n    ],\n    \"@typescript-eslint/consistent-type-imports\": \"warn\",\n    \"@typescript-eslint/prefer-nullish-coalescing\": \"warn\",\n    \"@typescript-eslint/explicit-function-return-type\": \"warn\"\n  }\n}\n"
  },
  {
    "path": ".github/workflows/deployment.yaml",
    "content": "name: Deploy 🚀\n\non:\n  push:\n    branches: ['main']\n  pull_request:\n    branches: ['main']\n\njobs:\n  prettier:\n    name: 🧪 Prettier\n    runs-on: ubuntu-latest\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v3\n\n      - name: 📥 Download deps\n        run: npm ci\n\n      - name: 🔍 Format\n        run: npm run format\n\n  eslint:\n    name: ✅ ESLint\n    runs-on: ubuntu-latest\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v3\n\n      - name: 📥 Download deps\n        run: npm ci\n\n      - name: 🪄 Lint\n        run: npm run lint\n\n  jest:\n    name: 🃏 Jest\n    runs-on: ubuntu-latest\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v3\n\n      - name: 📥 Download deps\n        run: npm ci\n\n      # ! uncomment this after you add test\n      # - name: 🔬 Test\n      # run: npm run test:ci\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n# python\n/.mypy_cache\n*.py\n\n.env\n\n.idea\n"
  },
  {
    "path": ".husky/pre-commit.bak",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nexec 1> /dev/tty\n\nnpx lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "content": "# testing\n/coverage\n\n# next.js\n/.next/\n/.vercel/\n/out/\n\n# production\n/build\n\n# compiled js functions\n/functions/lib/\n\n# python\n*.py\n.mypy_cache/\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"trailingComma\": \"none\"\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"WillLuke.nextjs.hasPrompted\": true\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "# https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile\nFROM node:18-alpine AS base\n\n# Install dependencies only when needed\nFROM base AS deps\n# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.\nRUN apk add --no-cache libc6-compat\nWORKDIR /app\n\n# Install dependencies based on the preferred package manager\nCOPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./\nRUN \\\n  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\\n  elif [ -f package-lock.json ]; then npm ci; \\\n  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\\n  else echo \"Lockfile not found.\" && exit 1; \\\n  fi\n\n\n# Rebuild the source code only when needed\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\n\n# Next.js collects completely anonymous telemetry data about general usage.\n# Learn more here: https://nextjs.org/telemetry\n# Uncomment the following line in case you want to disable telemetry during the build.\n# ENV NEXT_TELEMETRY_DISABLED=1\n\nRUN yarn prisma generate\n\nRUN \\\n  if [ -f yarn.lock ]; then yarn run build; \\\n  elif [ -f package-lock.json ]; then npm run build; \\\n  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \\\n  else echo \"Lockfile not found.\" && exit 1; \\\n  fi\n\n# Production image, copy all the files and run next\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV=production\n# Uncomment the following line in case you want to disable telemetry during runtime.\n# ENV NEXT_TELEMETRY_DISABLED=1\n\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\nCOPY --from=builder /app/public ./public\n\n# Set the correct permission for prerender cache\nRUN mkdir .next\nRUN chown nextjs:nodejs .next\n\n# Automatically leverage output traces to reduce image size\n# https://nextjs.org/docs/advanced-features/output-file-tracing\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static\n\nUSER nextjs\n\nEXPOSE 3000\n\nENV PORT=3000\n\n# server.js is created by next build from the standalone output\n# https://nextjs.org/docs/pages/api-reference/next-config-js/output\nENV HOSTNAME=\"0.0.0.0\"\nCMD [\"node\", \"server.js\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 ccrsxx\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Opencast\n\nA fully open source Twitter flavoured Farcaster client. Originally a fork of [ccrsxx/twitter-clone](https://github.com/ccrsxx/twitter-clone).\n\nThe goal of this project is to be a fully standalone Farcaster client that you can run on your own machine. It only depends on [stephancill/lazy-indexer](https://github.com/stephancill/lazy-indexer) and a connection to a Farcaster Hub.\n\n## Running it yourself\n\n### Prerequisites\n\n- [Docker](https://docs.docker.com/engine/install/)\n\n1. Clone the repo\n\n```\ngit clone git@github.com:stephancill/opencast.git\n```\n\n2. Copy .env.sample, rename it to .env and fill in the values\n\n```\ncp .env.sample .env\n```\n\n3. Run the Docker Compose file\n\n```\ndocker-compose up -d\n```\n\n4. Go to Opencast at http://localhost:3000 and log in. It will take a few moments to index your profile and might require you to refresh the page.\n\n## Development\n\n### Farcaster Indexer\n\nThis project depends on the Lazy Farcaster Indexer. Follow the instructions at [https://github.com/stephancill/lazy-indexer](https://github.com/stephancill/lazy-indexer) to set up an instance.\n\n### Local\n\nInstall dependencies\n\n```\nyarn install\n```\n\nFill in the environment variables\n\n```\ncp .env.dev.sample .env\n```\n\nRun the development server\n\n```\nyarn dev\n```\n\n## Todo\n\n- [ ] Feed\n  - [x] Reverse chronological feed\n  - [x] Pagination\n  - [x] Number of likes, comments, and reposts\n  - [ ] Recasts\n- [x] Cast detail\n  - [x] Number of likes, comments, and reposts\n  - [x] Paginated replies\n- [x] User profiles\n  - [x] Casts\n  - [x] Casts with replies\n  - [ ] Media\n  - [x] Likes\n  - [ ] Edit profile\n- [x] Auth\n- [x] Engagement actions\n- [x] Post creation\n  - [x] Text only\n  - [x] Media\n  - [x] Mentions\n  - [x] Embeds\n  - [x] Topic\n- [x] Post deletion\n- [ ] Search\n  - [x] User\n  - [ ] Topic\n  - [ ] Posts\n- [x] Channels (now called Topics)\n  - [x] Channel detail\n  - [x] Channel discovery\n  - [ ] Index channels\n- [x] Fix mobile layout\n- [ ] Rebrand\n  - [x] Renaming (casts -> tweets, etc)\n  - [x] Images\n  - [ ] Code\n- [x] Notifications\n  - [x] Badge counter\n  - [x] Notifications page\n- [ ] Optimize\n  - [ ] DB queries\n  - [ ] Bandwidth\n\n...\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.8'\nservices:\n  opencast:\n    build: ./\n    container_name: opencast\n    restart: unless-stopped\n    env_file:\n      # Set in .env\n      # APP_FID\n      # APP_MNENOMIC\n      - .env\n    environment:\n      DATABASE_URL: postgresql://indexer:password@postgres:5432/indexer\n      FC_HUB_URL: 'hub-grpc.pinata.cloud'\n      FC_HUB_USE_TLS: 'true'\n      NEXT_PUBLIC_FC_CLIENT_NAME: 'Opencast'\n      NEXT_PUBLIC_WALLETCONNECT_ID: '0fcda49e9f4acad4b84401373fbc5a4f'\n      NEXT_PUBLIC_URL: 'http://localhost:3000'\n      INDEXER_API_URL: 'http://lazy-indexer:3005'\n    ports:\n      - '3000:3000'\n    depends_on:\n      - lazy-indexer\n    networks:\n      - app-network\n\n  lazy-indexer:\n    image: stephancill/lazy-indexer\n    container_name: lazy-indexer\n    env_file:\n      # Set in .env\n      # TARGET_SIGNER_FID (usually same as APP_FID)\n      - .env\n    environment:\n      DATABASE_URL: postgresql://indexer:password@postgres:5432/indexer\n      REDIS_URL: redis://redis:6379\n      HUB_REST_URL: https://hub.pinata.cloud\n      HUB_RPC: hub-grpc.pinata.cloud\n      HUB_SSL: true\n      WORKER_CONCURRENCY: 5\n      LOG_LEVEL: debug\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n    ports:\n      - '3005:3005'\n    networks:\n      - app-network\n\n  postgres:\n    image: 'postgres:16-alpine'\n    restart: unless-stopped\n    ports:\n      - '5432:5432'\n    environment:\n      - POSTGRES_DB=indexer\n      - POSTGRES_USER=indexer\n      - POSTGRES_PASSWORD=password\n    volumes:\n      - postgres-data:/var/lib/postgresql/data\n    healthcheck:\n      test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']\n      interval: 5s # Check every 5 seconds for readiness\n      timeout: 5s # Allow up to 5 seconds for a response\n      retries: 3 # Fail after 3 unsuccessful attempts\n      start_period: 10s # Start checks after 10 seconds\n    networks:\n      - app-network\n\n  redis:\n    image: 'redis:7.2-alpine'\n    restart: unless-stopped\n    command: --loglevel warning --maxmemory-policy noeviction\n    volumes:\n      - redis-data:/data\n    ports:\n      - '6379:6379'\n    healthcheck:\n      test: ['CMD-SHELL', 'redis-cli ping']\n      interval: 5s # Check every 5 seconds\n      timeout: 5s # Allow up to 5 seconds for a response\n      retries: 3 # Fail after 3 unsuccessful attempts\n      start_period: 5s # Start health checks after 5 seconds\n    networks:\n      - app-network\n\nvolumes:\n  postgres-data:\n  redis-data:\n\nnetworks:\n  app-network:\n    driver: bridge\n"
  },
  {
    "path": "jest.config.js",
    "content": "// jest.config.js\nconst nextJest = require('next/jest');\n\nconst createJestConfig = nextJest({\n  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment\n  dir: './'\n});\n\n// Add any custom config to be passed to Jest\nconst customJestConfig = {\n  // Add more setup options before each test is run\n  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],\n  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work\n  modulePaths: ['<rootDir>/src'],\n  testEnvironment: 'jest-environment-jsdom',\n  // Math aliases too instead of just baseUrl\n  moduleNameMapper: {\n    '^@components(.*)$': '<rootDir>/src/components$1',\n    '^@lib(.*)$': '<rootDir>/src/lib$1',\n    '^@styles(.*)$': '<rootDir>/src/styles$1'\n  }\n};\n\n// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async\nmodule.exports = createJestConfig(customJestConfig);\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n  output: \"standalone\",\n  webpack: (config) => {\n    config.resolve.fallback = { fs: false, net: false, tls: false };\n    return config;\n  },\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: '*'\n      },\n      {\n        protocol: 'http',\n        hostname: '*'\n      }\n    ]\n  },\n  experimental: {\n    scrollRestoration: true\n  },\n  async headers() {\n    return [\n      {\n        // matching all API routes\n        source: '/api/:path*',\n        headers: [\n          { key: 'Access-Control-Allow-Credentials', value: 'true' },\n          { key: 'Access-Control-Allow-Origin', value: '*' },\n          {\n            key: 'Access-Control-Allow-Methods',\n            value: 'GET,DELETE,PATCH,POST,PUT'\n          },\n          {\n            key: 'Access-Control-Allow-Headers',\n            value:\n              'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'\n          }\n        ]\n      }\n    ];\n  }\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "nixpacks.toml",
    "content": "[phases.setup]\nnixPkgs = ['...', 'python3', 'gcc']\n\n[phases.install]\ncmds = ['yarn global add node-gyp', '...']"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"opencast\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"format\": \"prettier --check .\",\n    \"lint\": \"next lint\",\n    \"test\": \"jest --watch\",\n    \"test:ci\": \"jest --ci\",\n    \"prepare\": \"husky install\",\n    \"postinstall\": \"patch-package\"\n  },\n  \"dependencies\": {\n    \"@farcaster/core\": \"^0.13.3\",\n    \"@farcaster/hub-nodejs\": \"^0.10.3\",\n    \"@farcaster/hub-web\": \"^0.6.0\",\n    \"@frames.js/render\": \"^0.2.20\",\n    \"@headlessui/react\": \"^1.7.2\",\n    \"@heroicons/react\": \"^2.0.11\",\n    \"@noble/ed25519\": \"^2.0.0\",\n    \"@prisma/client\": \"^5.1.0\",\n    \"@rainbow-me/rainbowkit\": \"^2.1.3\",\n    \"@tanstack/react-query\": \"^5.49.2\",\n    \"clsx\": \"^1.2.1\",\n    \"firebase\": \"^9.9.4\",\n    \"framer-motion\": \"^7.2.1\",\n    \"frames.js\": \"^0.17.1\",\n    \"lodash\": \"^4.17.21\",\n    \"lru-cache\": \"^10.0.1\",\n    \"metadata-scraper\": \"^0.2.61\",\n    \"next\": \"^14.1.4\",\n    \"patch-package\": \"^8.0.0\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-hot-toast\": \"^2.3.0\",\n    \"react-qr-code\": \"^2.0.11\",\n    \"react-query\": \"^3.39.3\",\n    \"react-textarea-autosize\": \"^8.3.4\",\n    \"swr\": \"^1.3.0\",\n    \"validator\": \"^13.11.0\",\n    \"viem\": \"2.x\",\n    \"wagmi\": \"^2.10.9\"\n  },\n  \"devDependencies\": {\n    \"@testing-library/jest-dom\": \"^5.16.4\",\n    \"@testing-library/react\": \"^13.3.0\",\n    \"@testing-library/user-event\": \"^13.5.0\",\n    \"@types/lodash\": \"^4.14.197\",\n    \"@types/node\": \"18.6.4\",\n    \"@types/react\": \"18.0.16\",\n    \"@types/react-dom\": \"18.0.6\",\n    \"@types/validator\": \"^13.11.1\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.32.0\",\n    \"@typescript-eslint/parser\": \"^5.32.0\",\n    \"autoprefixer\": \"^10.4.8\",\n    \"dotenv\": \"^16.4.5\",\n    \"eslint\": \"8.21.0\",\n    \"eslint-config-next\": \"12.2.4\",\n    \"eslint-import-resolver-typescript\": \"^3.4.0\",\n    \"eslint-plugin-import\": \"^2.26.0\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"husky\": \"^8.0.1\",\n    \"jest\": \"^28.1.3\",\n    \"jest-environment-jsdom\": \"^28.1.3\",\n    \"lint-staged\": \"^13.0.3\",\n    \"postcss\": \"^8.4.16\",\n    \"prettier\": \"^2.7.1\",\n    \"prettier-plugin-tailwindcss\": \"^0.1.13\",\n    \"prisma\": \"^5.1.0\",\n    \"sass\": \"^1.54.4\",\n    \"tailwindcss\": \"^3.2.4\",\n    \"typescript\": \"^5.5.3\"\n  },\n  \"lint-staged\": {\n    \"**/*\": \"prettier --write --ignore-unknown\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"resolutions\": {\n    \"ffi-napi\": \"https://registry.yarnpkg.com/@favware/skip-dependency/-/skip-dependency-1.0.2.tgz\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {}\n  }\n};\n"
  },
  {
    "path": "prisma/schema.prisma",
    "content": "generator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel casts {\n  id                 String    @id @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at         DateTime  @default(now()) @db.Timestamptz(6)\n  updated_at         DateTime  @default(now()) @db.Timestamptz(6)\n  timestamp          DateTime  @db.Timestamptz(6)\n  deleted_at         DateTime? @db.Timestamptz(6)\n  pruned_at          DateTime? @db.Timestamptz(6)\n  fid                BigInt\n  parent_fid         BigInt?\n  hash               Bytes     @unique\n  root_parent_hash   Bytes?\n  parent_hash        Bytes?\n  root_parent_url    String?\n  parent_url         String?\n  text               String\n  signer             Bytes\n  embeds             Json      @default(\"[]\") @db.Json\n  mentions           Json      @default(\"[]\") @db.Json\n  mentions_positions Json      @default(\"[]\") @db.Json\n\n  @@index([timestamp], map: \"casts_timestamp_index\")\n}\n\nmodel fids {\n  fid              BigInt   @id\n  created_at       DateTime @default(now()) @db.Timestamptz(6)\n  updated_at       DateTime @default(now()) @db.Timestamptz(6)\n  registered_at    DateTime @db.Timestamptz(6)\n  custody_address  Bytes\n  recovery_address Bytes\n}\n\nmodel kysely_migration {\n  name      String @id @db.VarChar(255)\n  timestamp String @db.VarChar(255)\n}\n\nmodel kysely_migration_lock {\n  id        String @id @db.VarChar(255)\n  is_locked Int    @default(0)\n}\n\nmodel links {\n  id                String    @id @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at        DateTime  @default(now()) @db.Timestamptz(6)\n  updated_at        DateTime  @default(now()) @db.Timestamptz(6)\n  timestamp         DateTime  @db.Timestamptz(6)\n  deleted_at        DateTime? @db.Timestamptz(6)\n  pruned_at         DateTime? @db.Timestamptz(6)\n  fid               BigInt\n  target_fid        BigInt\n  display_timestamp DateTime? @db.Timestamptz(6)\n  type              String\n  hash              Bytes     @unique\n  signer            Bytes\n}\n\nmodel reactions {\n  id               String    @id @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at       DateTime  @default(now()) @db.Timestamptz(6)\n  updated_at       DateTime  @default(now()) @db.Timestamptz(6)\n  timestamp        DateTime  @db.Timestamptz(6)\n  deleted_at       DateTime? @db.Timestamptz(6)\n  pruned_at        DateTime? @db.Timestamptz(6)\n  fid              BigInt\n  target_cast_fid  BigInt?\n  type             Int       @db.SmallInt\n  hash             Bytes     @unique\n  target_cast_hash Bytes?\n  target_url       String?\n  signer           Bytes\n}\n\nmodel signers {\n  id            String?   @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at    DateTime  @default(now()) @db.Timestamptz(6)\n  updated_at    DateTime  @default(now()) @db.Timestamptz(6)\n  added_at      DateTime  @db.Timestamptz(6)\n  removed_at    DateTime? @db.Timestamptz(6)\n  fid           BigInt\n  requester_fid BigInt\n  key_type      Int       @db.SmallInt\n  metadata_type Int       @db.SmallInt\n  key           Bytes\n  metadata      Json      @db.Json\n\n  @@unique([fid, key], map: \"signers_fid_key_unique\")\n  @@index([fid], map: \"signers_fid_index\")\n  @@index([requester_fid], map: \"signers_requester_fid_index\")\n}\n\nmodel user_data {\n  id         String    @id @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at DateTime  @default(now()) @db.Timestamptz(6)\n  updated_at DateTime  @default(now()) @db.Timestamptz(6)\n  timestamp  DateTime  @db.Timestamptz(6)\n  deleted_at DateTime? @db.Timestamptz(6)\n  fid        BigInt\n  type       Int       @db.SmallInt\n  hash       Bytes     @unique\n  value      String\n  signer     Bytes\n\n  @@unique([fid, type], map: \"user_data_fid_type_unique\")\n}\n\n/// This model contains an expression index which requires additional setup for migrations. Visit https://pris.ly/d/expression-indexes for more info.\n/// This model contains an expression index which requires additional setup for migrations. Visit https://pris.ly/d/expression-indexes for more info.\n/// This model contains an expression index which requires additional setup for migrations. Visit https://pris.ly/d/expression-indexes for more info.\nmodel verifications {\n  id             String    @id @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at     DateTime  @default(now()) @db.Timestamptz(6)\n  updated_at     DateTime  @default(now()) @db.Timestamptz(6)\n  timestamp      DateTime  @db.Timestamptz(6)\n  deleted_at     DateTime? @db.Timestamptz(6)\n  fid            BigInt\n  hash           Bytes\n  signer_address Bytes\n  block_hash     Bytes\n  signature      Bytes\n\n  @@unique([signer_address, fid], map: \"verifications_signer_address_fid_unique\")\n  @@index([fid, timestamp], map: \"verifications_fid_timestamp_index\")\n}\n\nmodel hubs {\n  id              String   @id @default(dbgenerated(\"gen_random_uuid()\")) @db.Uuid\n  gossip_address  String\n  rpc_address     String\n  excluded_hashes String[]\n  count           Int      @default(0)\n  hub_version     String\n  network         String\n  app_version     String\n  timestamp       BigInt\n  created_at      DateTime @default(now()) @db.Timestamptz(6)\n  updated_at      DateTime @default(now()) @db.Timestamptz(6)\n}\n\nmodel storage {\n  id         String?  @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at DateTime @default(now()) @db.Timestamptz(6)\n  updated_at DateTime @default(now()) @db.Timestamptz(6)\n  rented_at  DateTime @db.Timestamptz(6)\n  expires_at DateTime @db.Timestamptz(6)\n  fid        BigInt\n  units      Int      @db.SmallInt\n  payer      Bytes\n\n  @@unique([fid, expires_at], map: \"storage_fid_expires_at_unique\")\n  @@index([fid, expires_at], map: \"storage_fid_expires_at_index\")\n}\n\nmodel targets {\n  id         String   @id @default(dbgenerated(\"generate_ulid()\")) @db.Uuid\n  created_at DateTime @default(now()) @db.Timestamptz(6)\n  updated_at DateTime @default(now()) @db.Timestamptz(6)\n  fid        BigInt   @unique(map: \"target_fid_unique\")\n}\n"
  },
  {
    "path": "public/site.webmanifest",
    "content": "{\n  \"name\": \"Opencast - Open Source Farcaster Client\",\n  \"short_name\": \"Opencast\",\n  \"description\": \"Fully open source Twitter flavoured Farcaster client\",\n  \"display\": \"standalone\",\n  \"start_url\": \"/\",\n  \"theme_color\": \"#fff\",\n  \"background_color\": \"#000000\",\n  \"orientation\": \"portrait\",\n  \"icons\": [\n    {\n      \"src\": \"/logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"/logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ]\n}"
  },
  {
    "path": "src/app/frames/route.ts",
    "content": "export { GET, POST } from '@frames.js/render/next';\n"
  },
  {
    "path": "src/components/aside/aside-footer.tsx",
    "content": "const footerLinks = [\n  ['GitHub', 'https://github.com/stephancill/twitter-farcaster-client']\n] as const;\n\nexport function AsideFooter(): JSX.Element {\n  return (\n    <footer\n      className='sticky top-[90vh] flex flex-col gap-3 text-center text-sm\n                 text-light-secondary dark:text-dark-secondary'\n    >\n      <nav className='flex flex-wrap justify-center gap-2'>\n        {footerLinks.map(([linkName, href]) => (\n          <a\n            className='custom-underline'\n            target='_blank'\n            rel='noreferrer'\n            href={href}\n            key={href}\n          >\n            {linkName}\n          </a>\n        ))}\n      </nav>\n      <div></div>\n      <p>Built by @stephancill. Use at own risk.</p>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "src/components/aside/aside-trends.tsx",
    "content": "import Link from 'next/link';\nimport cn from 'clsx';\nimport { motion } from 'framer-motion';\nimport { formatNumber } from '@lib/date';\nimport { preventBubbling } from '@lib/utils';\nimport { useTrends } from '@lib/api/trends';\nimport { Error } from '@components/ui/error';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { Button } from '@components/ui/button';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { Loading } from '@components/ui/loading';\nimport type { MotionProps } from 'framer-motion';\n\nexport const variants: MotionProps = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  transition: { duration: 0.8 }\n};\n\ntype AsideTrendsProps = {\n  inTrendsPage?: boolean;\n};\n\nexport function AsideTrends({ inTrendsPage }: AsideTrendsProps): JSX.Element {\n  const { data, loading } = useTrends(1, inTrendsPage ? 100 : 10, {\n    refreshInterval: 30000\n  });\n\n  const { trends, location } = data ?? {};\n\n  return (\n    <section\n      className={cn(\n        !inTrendsPage &&\n        'hover-animation rounded-2xl bg-main-sidebar-background'\n      )}\n    >\n      {loading ? (\n        <Loading />\n      ) : trends ? (\n        <motion.div\n          // className={cn('inner:px-4 inner:py-3', inTrendsPage && 'mt-0.5')}\n          {...variants}\n        >\n          {!inTrendsPage && (\n            <h2 className='text-xl font-extrabold'>Trends for you</h2>\n          )}\n          {trends.map(({ name, query, tweet_volume, url }) => (\n            <Link\n              href={url}\n              key={query}\n              className='hover-animation accent-tab hover-card relative \n                           flex cursor-not-allowed flex-col gap-0.5'\n            >\n              <div className='absolute right-2 top-2'>\n                <Button\n                  className='hover-animation group relative cursor-not-allowed p-2\n                               hover:bg-accent-blue/10 focus-visible:bg-accent-blue/20 \n                               focus-visible:!ring-accent-blue/80'\n                  onClick={preventBubbling()}\n                >\n                  <HeroIcon\n                    className='h-5 w-5 text-light-secondary group-hover:text-accent-blue \n                                 group-focus-visible:text-accent-blue dark:text-dark-secondary'\n                    iconName='EllipsisHorizontalIcon'\n                  />\n                  <ToolTip tip='More' />\n                </Button>\n              </div>\n              <p className='text-sm text-light-secondary dark:text-dark-secondary'>\n                Trending{' '}\n                {location === 'Worldwide'\n                  ? 'Worldwide'\n                  : `in ${location as string}`}\n              </p>\n              <p className='font-bold'>{name}</p>\n              <p className='text-sm text-light-secondary dark:text-dark-secondary'>\n                {formatNumber(tweet_volume)} tweets\n              </p>\n            </Link>\n          ))}\n          {!inTrendsPage && (\n            <Link\n              href='/trends'\n              className='custom-button accent-tab hover-card block w-full rounded-2xl\n                           rounded-t-none text-center text-main-accent'\n            >\n              Show more\n            </Link>\n          )}\n        </motion.div>\n      ) : (\n        <Error />\n      )}\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/aside/aside.tsx",
    "content": "import { useWindow } from '@lib/context/window-context';\nimport Link from 'next/link';\nimport type { ReactNode } from 'react';\nimport { User } from '../../lib/types/user';\nimport { UserSearchResult } from '../search/user-search-result';\nimport { AsideFooter } from './aside-footer';\nimport { SearchBar } from './search-bar';\n\ntype AsideProps = {\n  children: ReactNode;\n};\n\nexport function Aside({ children }: AsideProps): JSX.Element | null {\n  const { width } = useWindow();\n\n  if (width < 1024) return null;\n\n  return (\n    <aside className='flex w-96 flex-col gap-4 px-4 py-3 pt-1'>\n      <SearchBar<User>\n        urlBuilder={(input) =>\n          input.length > 0 ? `/api/search?q=${input}` : null\n        }\n        resultBuilder={(user, callback) => {\n          return (\n            <Link href={`/user/${user.username}`}>\n              <UserSearchResult user={user} key={user.id} callback={callback} />\n            </Link>\n          );\n        }}\n      />\n      {children}\n      <AsideFooter />\n    </aside>\n  );\n}\n"
  },
  {
    "path": "src/components/aside/search-bar.tsx",
    "content": "import { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport cn from 'clsx';\nimport { debounce } from 'lodash';\nimport { useRouter } from 'next/router';\nimport type { ChangeEvent, FormEvent, KeyboardEvent } from 'react';\nimport { useCallback, useRef, useState } from 'react';\nimport useSWR from 'swr';\nimport { fetchJSON } from '../../lib/fetch';\nimport { BaseResponse } from '../../lib/types/responses';\nimport { Loading } from '../ui/loading';\n\nexport type SearchBarProps<T> = {\n  urlBuilder: (query: string) => string | null;\n  resultBuilder: (data: T, callback: () => void) => JSX.Element;\n};\n\nexport function SearchBar<T>({\n  urlBuilder,\n  resultBuilder\n}: SearchBarProps<T>): JSX.Element {\n  const [inputValue, setInputValue] = useState('');\n  const [queryValue, setQueryValue] = useState('');\n  const [resultsVisible, setResultsVisible] = useState(false);\n\n  const blurTimeout = useRef<NodeJS.Timeout>();\n\n  const handleFocusEvent = () => {\n    clearTimeout(blurTimeout.current);\n    setResultsVisible(true);\n  };\n\n  const handleBlurEvent = () => {\n    blurTimeout.current = setTimeout(() => {\n      setResultsVisible(false);\n    }, 200);\n  };\n\n  const handleClickOnResult = () => {\n    clearTimeout(blurTimeout.current);\n    // your code for when a user clicks a search result\n    setResultsVisible(false);\n    setInputValue('');\n    setQueryValue('');\n  };\n\n  const { push } = useRouter();\n\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {\n    setInputValue(e.target.value);\n  };\n\n  const handleChangeDebounced = useCallback(\n    debounce((e) => {\n      setQueryValue(e.target.value);\n    }, 1000),\n    []\n  );\n\n  const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {\n    e.preventDefault();\n    // if (inputValue) void push(`/search?q=${inputValue}`);\n  };\n\n  const clearInputValue = (focus?: boolean) => (): void => {\n    if (focus) inputRef.current?.focus();\n    else inputRef.current?.blur();\n\n    setInputValue('');\n  };\n\n  const handleEscape = ({ key }: KeyboardEvent<HTMLInputElement>): void => {\n    if (key === 'Escape') clearInputValue()();\n  };\n\n  const { data, isValidating } = useSWR(\n    () => urlBuilder(queryValue),\n    async (url) => (await fetchJSON<BaseResponse<T[]>>(url)).result,\n    { revalidateOnFocus: false }\n  );\n\n  return (\n    <div className='hover-animation sticky top-0 z-10 flex-col bg-main-background py-2'>\n      {/* TODO: Use SearchBar component */}\n      <form className='' onSubmit={handleSubmit}>\n        <label\n          className='group flex items-center justify-between gap-4 rounded-full\n                   bg-main-search-background px-4 py-2 transition focus-within:bg-main-background\n                   focus-within:ring-2 focus-within:ring-main-accent'\n        >\n          <i>\n            <HeroIcon\n              className='h-5 w-5 text-light-secondary transition-colors \n                       group-focus-within:text-main-accent dark:text-dark-secondary'\n              iconName='MagnifyingGlassIcon'\n            />\n          </i>\n          <input\n            className='peer flex-1 bg-transparent outline-none \n                     placeholder:text-light-secondary dark:placeholder:text-dark-secondary'\n            type='text'\n            placeholder='Search Farcaster'\n            ref={inputRef}\n            value={inputValue}\n            onChange={(e) => {\n              handleChange(e);\n              handleChangeDebounced(e);\n            }}\n            onKeyUp={handleEscape}\n            onBlur={handleBlurEvent}\n            onFocus={handleFocusEvent}\n          />\n          <Button\n            className={cn(\n              'accent-tab scale-50 bg-main-accent p-1 opacity-0 transition hover:brightness-90 disabled:opacity-0',\n              inputValue &&\n                'focus:scale-100 focus:opacity-100 peer-focus:scale-100 peer-focus:opacity-100'\n            )}\n            onClick={clearInputValue(true)}\n            disabled={!inputValue}\n          >\n            <HeroIcon className='h-3 w-3 stroke-white' iconName='XMarkIcon' />\n          </Button>\n        </label>\n      </form>\n\n      {resultsVisible && (data || isValidating) && (\n        <div className='menu-container hover-animation absolute mt-1 w-full overflow-hidden rounded-2xl bg-main-background'>\n          {isValidating ? (\n            <div>{<Loading className='p-4' />}</div>\n          ) : data && data.length > 0 ? (\n            data?.map((result) =>\n              resultBuilder(result, () => {\n                handleClickOnResult();\n              })\n            )\n          ) : (\n            <div className='p-4'>No results</div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/aside/suggestions.tsx.bak",
    "content": "import Link from 'next/link';\nimport { motion } from 'framer-motion';\nimport {\n  doc,\n  limit,\n  query,\n  where,\n  orderBy,\n  documentId\n} from 'firebase/firestore';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useCollection } from '@lib/hooks/useCollection';\nimport { useDocument } from '@lib/hooks/useDocument';\nimport { usersCollection } from '@lib/firebase/collections';\nimport { UserCard } from '@components/user/user-card';\nimport { Loading } from '@components/ui/loading';\nimport { Error } from '@components/ui/error';\nimport { variants } from './aside-trends';\n\nexport function Suggestions(): JSX.Element {\n  const { randomSeed } = useAuth();\n\n  const { data: adminData, loading: adminLoading } = useDocument(\n    doc(usersCollection, 'Twt0A27bx9YcG4vu3RTsR7ifJzf2'),\n    { allowNull: true }\n  );\n\n  const { data: suggestionsData, loading: suggestionsLoading } = useCollection(\n    query(\n      usersCollection,\n      where(documentId(), '>=', randomSeed),\n      orderBy(documentId()),\n      limit(2)\n    ),\n    { allowNull: true }\n  );\n\n  return (\n    <section className='hover-animation rounded-2xl bg-main-sidebar-background'>\n      {adminLoading || suggestionsLoading ? (\n        <Loading className='flex h-52 items-center justify-center p-4' />\n      ) : suggestionsData ? (\n        <motion.div className='inner:px-4 inner:py-3' {...variants}>\n          <h2 className='text-xl font-bold'>Who to follow</h2>\n          {adminData && <UserCard {...adminData} />}\n          {suggestionsData?.map((userData) => (\n            <UserCard {...userData} key={userData.id} />\n          ))}\n          <Link href='/people'>\n            <a\n              className='custom-button accent-tab hover-card block w-full rounded-2xl\n                         rounded-t-none text-center text-main-accent'\n            >\n              Show more\n            </a>\n          </Link>\n        </motion.div>\n      ) : (\n        <Error />\n      )}\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/aside/trends.tsx",
    "content": "import { Error } from '@components/ui/error';\nimport { Loading } from '@components/ui/loading';\nimport { formatNumber } from '@lib/date';\nimport cn from 'clsx';\nimport type { MotionProps } from 'framer-motion';\nimport { motion } from 'framer-motion';\nimport Link from 'next/link';\nimport useSWR from 'swr';\nimport { fetchJSON } from '../../lib/fetch';\nimport { TrendsResponse } from '../../lib/types/trends';\nimport { NextImage } from '../ui/next-image';\n\nexport const variants: MotionProps = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  transition: { duration: 0.8 }\n};\n\ntype AsideTrendsProps = {\n  inTrendsPage?: boolean;\n};\n\nexport function AsideTrends({ inTrendsPage }: AsideTrendsProps): JSX.Element {\n  const { data: trends, isValidating: loading } = useSWR(\n    `/api/trends?limit=${inTrendsPage ? 20 : 5}`,\n    async (url: string) => (await fetchJSON<TrendsResponse>(url)).result,\n    {\n      revalidateOnFocus: false\n    }\n  );\n\n  return (\n    <section\n      className={cn(\n        !inTrendsPage &&\n        'hover-animation sticky top-[4.5rem] overflow-hidden rounded-2xl bg-main-sidebar-background'\n      )}\n    >\n      {loading ? (\n        <Loading />\n      ) : trends ? (\n        <motion.div\n          className={cn('flex flex-col gap-4', inTrendsPage && 'mt-0.5')}\n          {...variants}\n        >\n          {!inTrendsPage && (\n\n            <h2 className='px-4 pt-4 text-xl font-extrabold'>Trending topics</h2>\n          )}\n          <div className='flex flex-col'>\n            <div className='flex flex-col'>\n              {trends.map(({ topic: topic, volume }) => (\n                <Link href={`/topic?url=${topic?.url}`} key={topic?.url}>\n                  <div\n                    className='hover-animation accent-tab hover-card relative \n                           flex cursor-pointer flex-col gap-0.5 py-2 px-4'\n                  >\n                    <div className='flex items-center'>\n                      {topic?.image && (\n                        <div className='mr-2 overflow-hidden rounded-md border'>\n                          <NextImage\n                            imgClassName='object-fill'\n                            src={topic.image}\n                            alt={topic.name}\n                            // layout='fill'\n                            width={36}\n                            height={36}\n                          ></NextImage>\n                        </div>\n                      )}\n                      <div>\n                        <p className='font-bold'>{topic?.name}</p>\n                        <p className='text-sm text-light-secondary dark:text-dark-secondary'>\n                          {formatNumber(volume)} posts today\n                        </p>\n                      </div>\n                    </div>\n                  </div>\n                </Link>\n              ))}\n            </div>\n            {!inTrendsPage && (\n              <Link\n                href='/trends'\n                className='custom-button accent-tab hover-card block w-full rounded-2xl\n                           rounded-t-none text-center text-main-accent'\n              >\n                Show more\n              </Link>\n            )}\n          </div>\n        </motion.div>\n      ) : (\n        <Error />\n      )}\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/common/app-head.tsx",
    "content": "import Head from 'next/head';\n\nexport function AppHead(): JSX.Element {\n  return (\n    <Head>\n      <title>Opencast</title>\n      <meta name='og:title' content='Opencast' />\n      <link rel='icon' href='/favicon.ico' />\n      <link rel='manifest' href='/site.webmanifest' key='site-manifest' />\n      <meta name='twitter:card' content='summary_large_image' />\n    </Head>\n  );\n}\n"
  },
  {
    "path": "src/components/common/load-more.tsx",
    "content": "import { useEffect, useRef } from 'react';\n\n// TODO: Replace other load more components with this one\nexport function LoadMoreSentinel({\n  loadMore,\n  isLoading\n}: {\n  loadMore: () => void;\n  isLoading: boolean;\n}) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting && !isLoading) {\n          loadMore();\n        }\n      },\n      {\n        root: null,\n        rootMargin: '0px',\n        threshold: 1.0\n      }\n    );\n\n    if (ref.current) {\n      observer.observe(ref.current);\n    }\n\n    return () => {\n      if (ref.current) {\n        observer.unobserve(ref.current);\n      }\n    };\n  }, [loadMore, isLoading]);\n\n  return <div ref={ref}></div>;\n}\n"
  },
  {
    "path": "src/components/common/placeholder.tsx",
    "content": "import { CustomIcon } from '@components/ui/custom-icon';\nimport { SEO } from './seo';\n\nexport function Placeholder(): JSX.Element {\n  return (\n    <main className='flex min-h-screen items-center justify-center'>\n      <SEO\n        title='Opencast'\n        description='Fully open source Twitter flavoured Farcaster client.'\n        image='/banner.png'\n      />\n      <i>\n        <CustomIcon\n          className='h-20 w-20 text-[#1DA1F2]'\n          iconName='TwitterIcon'\n        />\n      </i>\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/components/common/seo.tsx",
    "content": "import { useRouter } from 'next/router';\nimport Head from 'next/head';\nimport { siteURL } from '@lib/env';\n\ntype MainLayoutProps = {\n  title: string;\n  image?: string;\n  description?: string;\n};\n\nexport function SEO({\n  title,\n  image,\n  description\n}: MainLayoutProps): JSX.Element {\n  const { asPath } = useRouter();\n\n  return (\n    <Head>\n      <title>{title}</title>\n      <meta name='og:title' content={title} />\n      {description && <meta name='description' content={description} />}\n      {description && <meta name='og:description' content={description} />}\n      {image && <meta property='og:image' content={image} />}\n      <meta\n        name='og:url'\n        content={`${siteURL}${asPath === '/' ? '' : asPath}`}\n      />\n    </Head>\n  );\n}\n"
  },
  {
    "path": "src/components/feed/tweet-feed.tsx",
    "content": "import useSWR from 'swr';\nimport useSWRInfinite from 'swr/infinite';\nimport { useAuth } from '../../lib/context/auth-context';\nimport {\n  PaginatedTweetsResponse,\n  TweetsResponse\n} from '../../lib/paginated-tweets';\nimport { FeedOrderingType } from '../../lib/types/feed';\nimport { populateTweetUsers } from '../../lib/types/tweet';\nimport { isPlural } from '../../lib/utils';\nimport { LoadMoreSentinel } from '../common/load-more';\nimport { Tweet } from '../tweet/tweet';\nimport { Error } from '../ui/error';\nimport { Loading } from '../ui/loading';\n\ninterface TweetFeedProps {\n  feedOrdering: FeedOrderingType;\n  apiEndpoint: string;\n}\n\nexport function TweetFeed({ feedOrdering, apiEndpoint }: TweetFeedProps) {\n  const { user, timelineCursor, setTimelineCursor } = useAuth();\n\n  const {\n    data: pages,\n    size,\n    setSize,\n    isValidating: loading,\n    error\n  } = useSWRInfinite<PaginatedTweetsResponse>(\n    (pageIndex, prevPage) => {\n      if (!user || !timelineCursor) return null;\n\n      if (prevPage && !prevPage.result?.nextPageCursor) return null;\n\n      const baseUrl = `${apiEndpoint}&limit=10&full=true&cursor=${timelineCursor.toISOString()}&ordering=${feedOrdering}`;\n\n      if (pageIndex === 0) {\n        return `${baseUrl}&skip=0`;\n      }\n\n      if (!prevPage?.result) return null;\n\n      return `${baseUrl}&skip=${prevPage.result.nextPageCursor}`;\n    },\n    {\n      revalidateOnFocus: false,\n      revalidateFirstPage: false\n    }\n  );\n  const hasMore = !!pages?.[size - 1]?.result?.tweets.length;\n\n  // Fetch new tweets every 20 seconds\n  const { data: newPage } = useSWR<TweetsResponse>(\n    !!pages && timelineCursor && feedOrdering === 'latest'\n      ? `${apiEndpoint}&cursor=${timelineCursor.toISOString()}&limit=100&after=true`\n      : null,\n    null,\n    { refreshInterval: 10_000 }\n  );\n\n  const onShowNewTweets = () => {\n    if (!newPage?.result?.tweets) return;\n    const cursor = new Date();\n    setTimelineCursor(cursor);\n  };\n\n  return (\n    <section className='mt-0.5 xs:mt-0'>\n      <>\n        {newPage?.result?.tweets && (newPage.result.tweets.length || 0) > 0 && (\n          <button\n            className='custom-button accent-tab hover-card border-bottom block w-full cursor-pointer rounded-none\n      border-b border-t-0 border-light-border text-center text-main-accent dark:border-dark-border'\n            onClick={onShowNewTweets}\n          >\n            Show {newPage.result.tweets.length} new cast\n            {isPlural(newPage.result.tweets.length) ? 's' : ''}\n          </button>\n        )}\n        {pages?.map(({ result }) => {\n          if (!result) return;\n          const { tweets, users } = result;\n          return tweets.map((tweet) => {\n            if (!users[tweet.createdBy]) {\n              return <></>;\n            }\n\n            return (\n              <Tweet\n                {...populateTweetUsers(tweet, users)}\n                user={users[tweet.createdBy]}\n                key={tweet.id}\n                usersMap={users}\n              />\n            );\n          });\n        })}\n        {hasMore && (\n          <LoadMoreSentinel\n            loadMore={() => {\n              setSize(size + 1);\n            }}\n            isLoading={loading}\n          ></LoadMoreSentinel>\n        )}\n      </>\n      {loading ? (\n        <Loading className='mt-5' />\n      ) : (\n        !pages && <Error message='Something went wrong' />\n      )}\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/frames/Frame.tsx",
    "content": "'use client';\n\nimport {\n  FarcasterFrameContext,\n  FarcasterSigner,\n  signFrameAction\n} from '@frames.js/render/farcaster';\nimport { useFrame } from '@frames.js/render/use-frame';\nimport { useAuth } from '@lib/context/auth-context';\nimport { Frame as FrameType } from 'frames.js';\nimport { useEffect, useState } from 'react';\nimport * as chains from 'viem/chains';\nimport {\n  useAccount,\n  useChainId,\n  useSendTransaction,\n  useSwitchChain\n} from 'wagmi';\nimport { FrameUI } from './frame-ui';\n\ntype FrameProps = {\n  url: string;\n  frame: FrameType;\n  frameContext: FarcasterFrameContext;\n};\n\nconst getChainFromId = (id: number): chains.Chain | undefined => {\n  return Object.values(chains).find((chain) => chain.id === id);\n};\n\nexport function Frame({ frame, frameContext, url }: FrameProps) {\n  const { user } = useAuth();\n  const { address: connectedAddress } = useAccount();\n  const [farcasterSigner, setFarcasterSigner] = useState<\n    FarcasterSigner | undefined\n  >(undefined);\n  const { sendTransactionAsync, sendTransaction } = useSendTransaction();\n  const currentChainId = useChainId();\n  const { switchChainAsync } = useSwitchChain();\n\n  useEffect(() => {\n    if (user?.keyPair) {\n      setFarcasterSigner({\n        fid: parseInt(user.id),\n        privateKey: user.keyPair.privateKey,\n        status: 'approved',\n        publicKey: user.keyPair.publicKey\n      });\n    } else {\n      setFarcasterSigner(undefined);\n    }\n  }, [user]);\n\n  const frameState = useFrame({\n    homeframeUrl: url,\n    frame,\n    frameActionProxy: '/frames',\n    connectedAddress,\n    frameGetProxy: '/frames',\n    frameContext,\n    signerState: {\n      hasSigner: farcasterSigner !== undefined,\n      signer: farcasterSigner,\n      onSignerlessFramePress: () => {\n        // Only run if `hasSigner` is set to `false`\n        // This is a good place to throw an error or prompt the user to login\n        // alert(\"A frame button was pressed without a signer. Perhaps you want to prompt a login\");\n      },\n      signFrameAction: signFrameAction\n    },\n    onTransaction: async ({ transactionData }) => {\n      // Switch to the chain that the transaction is on\n      const chainId = parseInt(transactionData.chainId.split(':')[1]);\n      if (chainId !== currentChainId) {\n        const newChain = await switchChainAsync?.({ chainId });\n        if (!newChain) {\n          console.error('Failed to switch network');\n          return null;\n        }\n      }\n\n      const hash = await sendTransactionAsync({\n        ...transactionData.params,\n        value: transactionData.params.value\n          ? BigInt(transactionData.params.value)\n          : undefined,\n        chainId: parseInt(transactionData.chainId.split(':')[1])\n      });\n      return hash || null;\n    }\n  });\n\n  return (\n    <div className='w-full overflow-hidden'>\n      <FrameUI frameState={frameState} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/frames/frame-ui.tsx",
    "content": "import { FrameUI as BaseFrameUI } from '@frames.js/render/ui';\nimport Image from 'next/image';\nimport React from 'react';\nimport cn from 'clsx';\nimport { HeroIcon } from '../ui/hero-icon';\n\ntype Props = React.ComponentProps<\n  typeof BaseFrameUI<{ className?: string; style?: React.CSSProperties }>\n>;\n\nconst components: Props['components'] = {\n  Button(\n    { frameButton, isDisabled, onPress, index, frameState },\n    stylingProps\n  ) {\n    return (\n      <button\n        {...stylingProps}\n        key={index}\n        className={cn(\n          'flex-1 rounded border p-2 text-sm text-light-primary hover:bg-gray-100 hover:text-light-primary dark:border-dark-border dark:text-white dark:hover:bg-gray-100/10 dark:hover:text-dark-primary',\n          (frameState.status === 'loading' || frameState.isImageLoading) &&\n            'cursor-default bg-gray-100',\n          stylingProps.className\n        )}\n        disabled={isDisabled}\n        onClick={onPress}\n        type='button'\n      >\n        {frameButton.action === 'mint' ? `⬗ ` : ''}\n        {frameButton.label}\n        {frameButton.action === 'tx' ? (\n          <HeroIcon\n            iconName='BoltIcon'\n            className='align-text-middle mb-[2px] ml-1 inline-block h-4 w-4 select-none overflow-visible text-gray-400'\n          ></HeroIcon>\n        ) : (\n          ''\n        )}\n        {frameButton.action === 'post_redirect' || frameButton.action === 'link'\n          ? ` ↗`\n          : ''}\n      </button>\n    );\n  },\n  MessageTooltip(props, stylingProps) {\n    return (\n      <div\n        {...stylingProps}\n        className={cn(\n          'absolute inset-x-2 bottom-2 rounded-sm border border-slate-100 shadow-md',\n          'flex items-center gap-2 p-2 text-sm',\n          props.status === 'error' && 'text-red-500',\n          stylingProps.className\n        )}\n      >\n        {props.status === 'error' ? (\n          <HeroIcon iconName='ExclamationCircleIcon' className='text-red-500' />\n        ) : (\n          <HeroIcon\n            iconName='InformationCircleIcon'\n            className='text-gray-500'\n          />\n        )}\n        {props.message}\n      </div>\n    );\n  },\n  LoadingScreen(props, stylingProps) {\n    return (\n      <div {...stylingProps}>\n        <div className='h-full w-full animate-pulse bg-gray-100 dark:bg-light-secondary'></div>\n      </div>\n    );\n  },\n  Image(props, stylingProps) {\n    if (props.status === 'frame-loading') {\n      return <div />;\n    }\n\n    return (\n      <Image\n        {...stylingProps}\n        src={props.src}\n        onLoad={props.onImageLoadEnd}\n        onError={props.onImageLoadEnd}\n        alt='Frame image'\n        sizes='100vw'\n        height={0}\n        width={0}\n      />\n    );\n  }\n};\n\nconst theme: Props['theme'] = {\n  ButtonsContainer: {\n    className: 'flex gap-[8px] px-2 pb-2'\n  },\n  Root: {\n    className:\n      'flex flex-col w-full gap-2 border rounded-lg overflow-hidden relative'\n  },\n  Error: {\n    className:\n      'flex text-red-500 text-sm p-2 border border-red-500 rounded-md shadow-md aspect-square justify-center items-center'\n  },\n  LoadingScreen: {\n    className:\n      'absolute top-0 left-0 right-0 bottom-0 z-10 bg-white dark:bg-light-primary'\n  },\n  Image: {\n    className: 'w-full object-cover max-h-full'\n  },\n  ImageContainer: {\n    className: 'relative w-full h-full border-b border-gray-300 overflow-hidden'\n  },\n  TextInput: {\n    className: 'p-[6px] border rounded border-gray-300 box-border w-full'\n  },\n  TextInputContainer: {\n    className: 'w-full px-2'\n  }\n};\n\nexport function FrameUI(props: Props) {\n  return <BaseFrameUI {...props} components={components} theme={theme} />;\n}\n"
  },
  {
    "path": "src/components/home/main-container.tsx",
    "content": "import cn from 'clsx';\nimport type { ReactNode } from 'react';\n\ntype MainContainerProps = {\n  children: ReactNode;\n  className?: string;\n};\n\nexport function MainContainer({\n  children,\n  className\n}: MainContainerProps): JSX.Element {\n  return (\n    <main\n      className={cn(\n        `hover-animation flex min-h-screen w-full max-w-xl flex-col border-x-0\n         border-light-border pb-96 dark:border-dark-border xs:border-x`,\n        className\n      )}\n    >\n      {children}\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/components/home/main-header.tsx",
    "content": "import cn from 'clsx';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { MobileSidebar } from '@components/sidebar/mobile-sidebar';\nimport type { ReactNode } from 'react';\nimport type { IconName } from '@components/ui/hero-icon';\nimport { NextImage } from '../ui/next-image';\n\ntype HomeHeaderProps = {\n  tip?: string;\n  title?: string;\n  description?: string;\n  children?: ReactNode;\n  imageUrl?: string;\n  iconName?: IconName;\n  className?: string;\n  disableSticky?: boolean;\n  useActionButton?: boolean;\n  useMobileSidebar?: boolean;\n  action?: () => void;\n};\n\nexport function MainHeader({\n  tip,\n  title,\n  description,\n  children,\n  imageUrl,\n  iconName,\n  className,\n  disableSticky,\n  useActionButton,\n  useMobileSidebar,\n  action\n}: HomeHeaderProps): JSX.Element {\n  return (\n    <header\n      className={cn(\n        'hover-animation even z-10 bg-main-background/60 px-4 py-2 backdrop-blur-md',\n        !disableSticky && 'sticky top-0',\n        className ?? 'flex items-center gap-6'\n      )}\n    >\n      {useActionButton && (\n        <Button\n          className='dark-bg-tab group relative p-2 hover:bg-light-primary/10 active:bg-light-primary/20 \n                     dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20'\n          onClick={action}\n        >\n          <HeroIcon\n            className='h-5 w-5'\n            iconName={iconName ?? 'ArrowLeftIcon'}\n          />\n          <ToolTip tip={tip ?? 'Back'} />\n        </Button>\n      )}\n      {title && (\n        <div className='flex items-center'>\n          {imageUrl && (\n            <span className='mr-2 inline flex-shrink-0 flex-grow-0 overflow-hidden rounded-md'>\n              <NextImage\n                src={imageUrl}\n                alt={title || 'image'}\n                objectFit='contain'\n                width={48}\n                height={48}\n              ></NextImage>\n            </span>\n          )}\n          <div className='flex flex-col'>\n            {useMobileSidebar && <MobileSidebar />}\n            <h2 className='text-xl font-bold' key={title}>\n              {title}\n            </h2>\n\n            {description && (\n              <p\n                className='text-light-secondary dark:text-dark-secondary'\n                key={description}\n              >\n                {description}\n              </p>\n            )}\n          </div>\n        </div>\n      )}\n\n      {children}\n    </header>\n  );\n}\n"
  },
  {
    "path": "src/components/home/update-username.tsx.bak",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport { useState, useEffect } from 'react';\nimport { toast } from 'react-hot-toast';\nimport { checkUsernameAvailability, updateUsername } from '@lib/firebase/utils';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport { isValidUsername } from '@lib/validation';\nimport { sleep } from '@lib/utils';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { Modal } from '@components/modal/modal';\nimport { UsernameModal } from '@components/modal/username-modal';\nimport { InputField } from '@components/input/input-field';\nimport type { FormEvent, ChangeEvent } from 'react';\n\nexport function UpdateUsername(): JSX.Element {\n  const [alreadySet, setAlreadySet] = useState(false);\n  const [available, setAvailable] = useState(false);\n  const [loading, setLoading] = useState(false);\n  const [visited, setVisited] = useState(false);\n  const [inputValue, setInputValue] = useState('');\n  const [errorMessage, setErrorMessage] = useState('');\n\n  const { user } = useAuth();\n  const { open, openModal, closeModal } = useModal();\n\n  useEffect(() => {\n    const checkAvailability = async (value: string): Promise<void> => {\n      const empty = await checkUsernameAvailability(value);\n\n      if (empty) setAvailable(true);\n      else {\n        setAvailable(false);\n        setErrorMessage('This username has been taken. Please choose another.');\n      }\n    };\n\n    if (!visited && inputValue.length > 0) setVisited(true);\n\n    if (visited) {\n      if (errorMessage) setErrorMessage('');\n\n      const error = isValidUsername(user?.username as string, inputValue);\n\n      if (error) {\n        setAvailable(false);\n        setErrorMessage(error);\n      } else void checkAvailability(inputValue);\n    }\n  }, [inputValue]);\n\n  useEffect(() => {\n    if (!user?.updatedAt) openModal();\n    else setAlreadySet(true);\n  }, []);\n\n  const changeUsername = async (\n    e: FormEvent<HTMLFormElement>\n  ): Promise<void> => {\n    e.preventDefault();\n\n    if (!available) return;\n\n    setLoading(true);\n\n    await sleep(500);\n\n    await updateUsername(user?.id as string, inputValue);\n\n    closeModal();\n\n    setLoading(false);\n\n    setInputValue('');\n    setVisited(false);\n    setAvailable(false);\n\n    toast.success('Username updated successfully');\n  };\n\n  const cancelUpdateUsername = (): void => {\n    closeModal();\n    if (!alreadySet) void updateUsername(user?.id as string);\n  };\n\n  const handleChange = ({\n    target: { value }\n  }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>\n    setInputValue(value);\n\n  return (\n    <>\n      <Modal\n        modalClassName='flex flex-col gap-6 max-w-xl bg-main-background w-full p-8 rounded-2xl h-[576px]'\n        open={open}\n        closeModal={cancelUpdateUsername}\n      >\n        <UsernameModal\n          loading={loading}\n          available={available}\n          alreadySet={alreadySet}\n          changeUsername={changeUsername}\n          cancelUpdateUsername={cancelUpdateUsername}\n        >\n          <InputField\n            label='Username'\n            inputId='username'\n            inputValue={inputValue}\n            errorMessage={errorMessage}\n            handleChange={handleChange}\n          />\n        </UsernameModal>\n      </Modal>\n      <Button\n        className='dark-bg-tab group relative p-2 hover:bg-light-primary/10\n                   active:bg-light-primary/20 dark:hover:bg-dark-primary/10 \n                   dark:active:bg-dark-primary/20'\n        onClick={openModal}\n      >\n        <HeroIcon className='h-5 w-5' iconName='SparklesIcon' />\n        <ToolTip tip='Top tweets' />\n      </Button>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/input/image-preview.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport cn from 'clsx';\nimport { useModal } from '@lib/hooks/useModal';\nimport { preventBubbling } from '@lib/utils';\nimport { ImageModal } from '@components/modal/image-modal';\nimport { Modal } from '@components/modal/modal';\nimport { NextImage } from '@components/ui/next-image';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport type { MotionProps } from 'framer-motion';\nimport type { ImagesPreview, ImageData } from '@lib/types/file';\n\ntype ImagePreviewProps = {\n  tweet?: boolean;\n  viewTweet?: boolean;\n  previewCount: number;\n  imagesPreview: ImagesPreview;\n  removeImage?: (targetId: string) => () => void;\n};\n\nconst variants: MotionProps = {\n  initial: { opacity: 0, scale: 0.5 },\n  animate: {\n    opacity: 1,\n    scale: 1,\n    transition: { duration: 0.3 }\n  },\n  exit: { opacity: 0, scale: 0.5 },\n  transition: { type: 'spring', duration: 0.5 }\n};\n\ntype PostImageBorderRadius = Record<number, string[]>;\n\nconst postImageBorderRadius: Readonly<PostImageBorderRadius> = {\n  1: ['rounded-2xl'],\n  2: ['rounded-tl-2xl rounded-bl-2xl', 'rounded-tr-2xl rounded-br-2xl'],\n  3: ['rounded-tl-2xl rounded-bl-2xl', 'rounded-tr-2xl', 'rounded-br-2xl'],\n  4: ['rounded-tl-2xl', 'rounded-tr-2xl', 'rounded-bl-2xl', 'rounded-br-2xl']\n};\n\nexport function ImagePreview({\n  tweet,\n  viewTweet,\n  previewCount,\n  imagesPreview,\n  removeImage\n}: ImagePreviewProps): JSX.Element {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [selectedImage, setSelectedImage] = useState<ImageData | null>(null);\n\n  const { open, openModal, closeModal } = useModal();\n\n  useEffect(() => {\n    const imageData = imagesPreview[selectedIndex];\n    setSelectedImage(imageData);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [selectedIndex]);\n\n  const handleSelectedImage = (index: number) => () => {\n    setSelectedIndex(index);\n    openModal();\n  };\n\n  const handleNextIndex = (type: 'prev' | 'next') => () => {\n    const nextIndex =\n      type === 'prev'\n        ? selectedIndex === 0\n          ? previewCount - 1\n          : selectedIndex - 1\n        : selectedIndex === previewCount - 1\n          ? 0\n          : selectedIndex + 1;\n\n    setSelectedIndex(nextIndex);\n  };\n\n  const isTweet = tweet ?? viewTweet;\n\n  return (\n    <div\n      className={cn(\n        'grid grid-cols-2 grid-rows-2 rounded-2xl',\n        viewTweet\n          ? 'h-[51vw] xs:h-[42vw] md:h-[305px]'\n          : 'h-[42vw] xs:h-[37vw] md:h-[271px]',\n        isTweet ? 'mt-2 gap-0.5' : 'gap-3'\n      )}\n    >\n      <Modal\n        modalClassName={cn(\n          'flex justify-center w-full items-center relative',\n          isTweet && 'h-full'\n        )}\n        open={open}\n        closeModal={closeModal}\n        closePanelOnClick\n      >\n        <ImageModal\n          tweet={isTweet}\n          imageData={selectedImage as ImageData}\n          previewCount={previewCount}\n          selectedIndex={selectedIndex}\n          handleNextIndex={handleNextIndex}\n        />\n      </Modal>\n      {imagesPreview.map(({ id, src, alt }, index) => (\n        <button\n          className={cn(\n            'accent-tab relative transition-shadow',\n            isTweet\n              ? postImageBorderRadius[previewCount][index]\n              : 'rounded-2xl',\n            {\n              'col-span-2 row-span-2': previewCount === 1,\n              'row-span-2':\n                previewCount === 2 || (index === 0 && previewCount === 3)\n            }\n          )}\n          onClick={preventBubbling(handleSelectedImage(index))}\n          key={id}\n        >\n          <div\n            className='flex h-full w-full cursor-pointer \n                         justify-center transition hover:brightness-75 hover:duration-200'\n          >\n            <img className={cn('rounded-2xl object-cover')} src={src} alt={alt} />\n          </div>\n          {removeImage && (\n            <Button\n              className='group absolute left-0 top-0 translate-x-1 translate-y-1\n                           bg-light-primary/75 p-1 backdrop-blur-sm \n                           hover:bg-image-preview-hover/75'\n              onClick={preventBubbling(removeImage(id))}\n            >\n              <HeroIcon className='h-5 w-5 text-white' iconName='XMarkIcon' />\n              <ToolTip className='translate-y-2' tip='Remove' />\n            </Button>\n          )}\n        </button>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/input/input-accent-radio.tsx",
    "content": "import cn from 'clsx';\nimport { useTheme } from '@lib/context/theme-context';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport type { Accent } from '@lib/types/theme';\n\ntype InputAccentRadioProps = {\n  type: Accent;\n};\n\ntype InputAccentData = Record<Accent, string>;\n\nconst InputColors: Readonly<InputAccentData> = {\n  yellow:\n    'bg-accent-yellow hover:ring-accent-yellow/10 active:ring-accent-yellow/20',\n  blue: 'bg-accent-blue hover:ring-accent-blue/10 active:ring-accent-blue/20',\n  pink: 'bg-accent-pink hover:ring-accent-pink/10 active:ring-accent-pink/20',\n  purple:\n    'bg-accent-purple hover:ring-accent-purple/10 active:ring-accent-purple/20',\n  orange:\n    'bg-accent-orange hover:ring-accent-orange/10 active:ring-accent-orange/20',\n  green:\n    'bg-accent-green hover:ring-accent-green/10 active:ring-accent-green/20'\n};\n\nexport function InputAccentRadio({ type }: InputAccentRadioProps): JSX.Element {\n  const { accent, changeAccent } = useTheme();\n\n  const bgColor = InputColors[type];\n  const isChecked = type === accent;\n\n  return (\n    <label\n      className={cn(\n        `hover-animation flex h-10 w-10 cursor-pointer items-center justify-center\n         rounded-full hover:ring`,\n        bgColor\n      )}\n      htmlFor={type}\n    >\n      <input\n        className='peer absolute h-0 w-0 opacity-0'\n        id={type}\n        type='radio'\n        name='accent'\n        value={type}\n        checked={isChecked}\n        onChange={changeAccent}\n      />\n      <i className='text-white peer-checked:inner:opacity-100'>\n        <HeroIcon\n          className='h-6 w-6 opacity-0 transition-opacity duration-200'\n          iconName='CheckIcon'\n        />\n      </i>\n    </label>\n  );\n}\n"
  },
  {
    "path": "src/components/input/input-field.tsx",
    "content": "import cn from 'clsx';\nimport type { User, EditableData } from '@lib/types/user';\nimport type { KeyboardEvent, ChangeEvent } from 'react';\n\nexport type InputFieldProps = {\n  label: string;\n  inputId: EditableData | Extract<keyof User, 'username'>;\n  inputValue: string | null;\n  inputLimit?: number;\n  useTextArea?: boolean;\n  errorMessage?: string;\n  handleChange: (\n    e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n  ) => void;\n  handleKeyboardShortcut?: ({\n    key,\n    ctrlKey\n  }: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;\n};\n\nexport function InputField({\n  label,\n  inputId,\n  inputValue,\n  inputLimit,\n  useTextArea,\n  errorMessage,\n  handleChange,\n  handleKeyboardShortcut\n}: InputFieldProps): JSX.Element {\n  const slicedInputValue = inputValue?.slice(0, inputLimit) ?? '';\n\n  const inputLength = slicedInputValue.length;\n  const isHittingInputLimit = inputLimit && inputLength > inputLimit;\n\n  return (\n    <div className='flex flex-col gap-1'>\n      <div\n        className={cn(\n          'relative rounded ring-1 transition-shadow duration-200',\n          errorMessage\n            ? 'ring-accent-red'\n            : `ring-light-line-reply focus-within:ring-2 \n                 focus-within:!ring-main-accent dark:ring-dark-border`\n        )}\n      >\n        {useTextArea ? (\n          <textarea\n            className='peer mt-6 w-full resize-none bg-inherit px-3 pb-1\n                       placeholder-transparent outline-none transition'\n            id={inputId}\n            placeholder={inputId}\n            onChange={!isHittingInputLimit ? handleChange : undefined}\n            onKeyUp={handleKeyboardShortcut}\n            value={slicedInputValue}\n            rows={3}\n          />\n        ) : (\n          <input\n            className='peer mt-6 w-full bg-inherit px-3 pb-1\n                       placeholder-transparent outline-none transition'\n            id={inputId}\n            type='text'\n            placeholder={inputId}\n            onChange={!isHittingInputLimit ? handleChange : undefined}\n            value={slicedInputValue}\n            onKeyUp={handleKeyboardShortcut}\n          />\n        )}\n        <label\n          className={cn(\n            `group-peer absolute left-3 translate-y-1 bg-main-background text-sm\n             text-light-secondary transition-all peer-placeholder-shown:translate-y-3\n             peer-placeholder-shown:text-lg peer-focus:translate-y-1 peer-focus:text-sm\n             dark:text-dark-secondary`,\n            errorMessage\n              ? '!text-accent-red peer-focus:text-accent-red'\n              : 'peer-focus:text-main-accent'\n          )}\n          htmlFor={inputId}\n        >\n          {label}\n        </label>\n        {inputLimit && (\n          <span\n            className={cn(\n              `absolute right-3 top-0 translate-y-1 text-sm text-light-secondary transition-opacity \n               duration-200 peer-focus:visible peer-focus:opacity-100 dark:text-dark-secondary`,\n              errorMessage ? 'visible opacity-100' : 'invisible opacity-0'\n            )}\n          >\n            {inputLength} / {inputLimit}\n          </span>\n        )}\n      </div>\n      {errorMessage && (\n        <p className='text-sm text-accent-red'>{errorMessage}</p>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/input/input-form.tsx",
    "content": "import { useEffect } from 'react';\nimport TextArea from 'react-textarea-autosize';\nimport { motion } from 'framer-motion';\nimport { useModal } from '@lib/hooks/useModal';\nimport { Modal } from '@components/modal/modal';\nimport { ActionModal } from '@components/modal/action-modal';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { Button } from '@components/ui/button';\nimport type {\n  ReactNode,\n  RefObject,\n  ChangeEvent,\n  KeyboardEvent,\n  ClipboardEvent\n} from 'react';\nimport type { Variants } from 'framer-motion';\n\ntype InputFormProps = {\n  modal?: boolean;\n  formId: string;\n  loading: boolean;\n  visited: boolean;\n  reply?: boolean;\n  children: ReactNode;\n  inputRef: RefObject<HTMLTextAreaElement>;\n  inputValue: string;\n  replyModal?: boolean;\n  isValidTweet: boolean;\n  isUploadingImages: boolean;\n  sendTweet: () => Promise<void>;\n  handleFocus: () => void;\n  discardTweet: () => void;\n  handleChange: ({\n    target: { value }\n  }: ChangeEvent<HTMLTextAreaElement>) => void;\n  handleImageUpload: (\n    e: ChangeEvent<HTMLInputElement> | ClipboardEvent<HTMLTextAreaElement>\n  ) => void;\n};\n\nconst variants: Variants[] = [\n  {\n    initial: { y: -25, opacity: 0 },\n    animate: { y: 0, opacity: 1, transition: { type: 'spring' } }\n  },\n  {\n    initial: { x: 25, opacity: 0 },\n    animate: { x: 0, opacity: 1, transition: { type: 'spring' } }\n  }\n];\n\nexport const [fromTop, fromBottom] = variants;\n\nexport function InputForm({\n  modal,\n  reply,\n  formId,\n  loading,\n  visited,\n  children,\n  inputRef,\n  replyModal,\n  inputValue,\n  isValidTweet,\n  isUploadingImages,\n  sendTweet,\n  handleFocus,\n  discardTweet,\n  handleChange,\n  handleImageUpload\n}: InputFormProps): JSX.Element {\n  const { open, openModal, closeModal } = useModal();\n\n  useEffect(() => handleShowHideNav(true), []);\n\n  const handleKeyboardShortcut = ({\n    key,\n    ctrlKey\n  }: KeyboardEvent<HTMLTextAreaElement>): void => {\n    if (!modal && key === 'Escape')\n      if (isValidTweet) {\n        inputRef.current?.blur();\n        openModal();\n      } else discardTweet();\n    else if (ctrlKey && key === 'Enter' && isValidTweet) void sendTweet();\n  };\n\n  const handleShowHideNav = (blur?: boolean) => (): void => {\n    const sidebar = document.getElementById('sidebar') as HTMLElement;\n\n    if (!sidebar) return;\n\n    if (blur) {\n      setTimeout(() => (sidebar.style.opacity = ''), 200);\n      return;\n    }\n\n    if (window.innerWidth < 500) sidebar.style.opacity = '0';\n  };\n\n  const handleFormFocus = (): void => {\n    handleShowHideNav()();\n    handleFocus();\n  };\n\n  const handleClose = (): void => {\n    discardTweet();\n    closeModal();\n  };\n\n  // const isVisibilityShown = visited && !reply && !replyModal && !loading;\n\n  return (\n    <div className='flex min-h-[48px] w-full flex-col justify-center gap-4'>\n      <Modal\n        modalClassName='max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={open}\n        closeModal={closeModal}\n      >\n        <ActionModal\n          title='Discard Tweet?'\n          description='This can’t be undone and you’ll lose your draft.'\n          mainBtnClassName='bg-accent-red hover:bg-accent-red/90 active:bg-accent-red/75'\n          mainBtnLabel='Discard'\n          action={handleClose}\n          closeModal={closeModal}\n        />\n      </Modal>\n      <div className='flex flex-col gap-6'>\n        {/* {isVisibilityShown && (\n          <motion.button\n            type='button'\n            className='custom-button accent-tab accent-bg-tab flex cursor-not-allowed items-center gap-1\n                       self-start border border-light-line-reply px-3 py-0 text-main-accent\n                       hover:bg-main-accent/10 active:bg-main-accent/20 dark:border-light-secondary'\n            {...fromTop}\n          >\n            <p className='font-bold'>Everyone</p>\n            <HeroIcon className='h-4 w-4' iconName='ChevronDownIcon' />\n          </motion.button>\n        )} */}\n        <div className='flex items-center gap-3'>\n          <TextArea\n            id={formId}\n            className='w-full min-w-0 resize-none bg-transparent text-xl outline-none\n                       placeholder:text-light-secondary dark:placeholder:text-dark-secondary'\n            value={inputValue}\n            placeholder={\n              reply || replyModal ? 'Cast your reply' : \"What's happening?\"\n            }\n            onBlur={handleShowHideNav(true)}\n            minRows={loading ? 1 : modal && !isUploadingImages ? 3 : 1}\n            maxRows={isUploadingImages ? 5 : 15}\n            onFocus={handleFormFocus}\n            onPaste={handleImageUpload}\n            onKeyUp={handleKeyboardShortcut}\n            onChange={handleChange}\n            ref={inputRef}\n          />\n          {reply && !visited && (\n            <Button\n              className='cursor-pointer bg-main-accent px-4 py-1.5 font-bold text-white opacity-50'\n              onClick={handleFocus}\n            >\n              Reply\n            </Button>\n          )}\n        </div>\n      </div>\n      {children}\n      {/* {isVisibilityShown && (\n        <motion.div\n          className='flex border-b border-light-border pb-2 dark:border-dark-border'\n          {...fromBottom}\n        >\n          <button\n            type='button'\n            className='custom-button accent-tab accent-bg-tab flex cursor-not-allowed items-center gap-1 px-3\n                       py-0 text-main-accent hover:bg-main-accent/10 active:bg-main-accent/20'\n          >\n            <HeroIcon className='h-4 w-4' iconName='GlobeAmericasIcon' />\n            <p className='font-bold'>Everyone can reply</p>\n          </button>\n        </motion.div>\n      )} */}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/input/input-options.tsx",
    "content": "import { useRef } from 'react';\nimport { motion } from 'framer-motion';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { variants } from './input';\nimport { ProgressBar } from './progress-bar';\nimport type { ChangeEvent, ClipboardEvent } from 'react';\nimport type { IconName } from '@components/ui/hero-icon';\n\ntype Options = {\n  name: string;\n  iconName: IconName;\n  disabled: boolean;\n  onClick?: () => void;\n}[];\n\ntype InputOptionsProps = {\n  reply?: boolean;\n  modal?: boolean;\n  inputLimit: number;\n  inputLength: number;\n  isValidTweet: boolean;\n  isCharLimitExceeded: boolean;\n  handleImageUpload: (\n    e: ChangeEvent<HTMLInputElement> | ClipboardEvent<HTMLTextAreaElement>\n  ) => void;\n  options: Readonly<Options>;\n};\n\nexport function InputOptions({\n  reply,\n  modal,\n  inputLimit,\n  inputLength,\n  isValidTweet,\n  isCharLimitExceeded,\n  handleImageUpload,\n  options\n}: InputOptionsProps): JSX.Element {\n  const inputFileRef = useRef<HTMLInputElement>(null);\n\n  const imgOnClick = (): void => inputFileRef.current?.click();\n\n  let filteredOptions = options;\n\n  return (\n    <motion.div className='flex justify-between' {...variants}>\n      <div\n        className='flex text-main-accent [&>button:nth-child(n+4)]:hidden \n                   xs:[&>button:nth-child(n+6)]:hidden md:[&>button]:!block'\n      >\n        <input\n          className='hidden'\n          type='file'\n          accept='image/*'\n          onChange={handleImageUpload}\n          ref={inputFileRef}\n          multiple\n        />\n        {filteredOptions.map(({ name, iconName, disabled, onClick }, index) => (\n          <Button\n            className='accent-tab accent-bg-tab group relative rounded-full p-2 \n                       hover:bg-main-accent/10 active:bg-main-accent/20'\n            onClick={onClick ? onClick : imgOnClick}\n            disabled={disabled}\n            key={name}\n          >\n            <HeroIcon className='h-5 w-5' iconName={iconName} />\n            <ToolTip tip={name} modal={modal} />\n          </Button>\n        ))}\n      </div>\n      <div className='flex items-center gap-4'>\n        <motion.div\n          className='flex items-center gap-4'\n          animate={\n            inputLength ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0 }\n          }\n        >\n          <ProgressBar\n            modal={modal}\n            inputLimit={inputLimit}\n            inputLength={inputLength}\n            isCharLimitExceeded={isCharLimitExceeded}\n          />\n          {!reply && (\n            <>\n              <i className='hidden h-8 w-[1px] bg-[#B9CAD3] dark:bg-[#3E4144] xs:block' />\n              <Button\n                className='group relative hidden rounded-full border border-light-line-reply p-[1px]\n                           text-main-accent dark:border-light-secondary xs:block'\n                disabled\n              >\n                <HeroIcon className='h-5 w-5' iconName='PlusIcon' />\n                <ToolTip tip='Add' modal={modal} />\n              </Button>\n            </>\n          )}\n        </motion.div>\n        <Button\n          type='submit'\n          className='accent-tab bg-main-accent px-4 py-1.5 font-bold text-white\n                     enabled:hover:bg-main-accent/90\n                     enabled:active:bg-main-accent/75'\n          disabled={!isValidTweet}\n        >\n          {reply ? 'Reply' : 'Cast'}\n        </Button>\n      </div>\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "src/components/input/input-theme-radio.tsx",
    "content": "import cn from 'clsx';\nimport { useTheme } from '@lib/context/theme-context';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport type { Theme } from '@lib/types/theme';\n\ntype InputThemeRadioProps = {\n  type: Theme;\n  label: string;\n};\n\ntype InputThemeData = Record<\n  Theme,\n  {\n    textColor: string;\n    backgroundColor: string;\n    iconBorderColor: string;\n    hoverBackgroundColor: string;\n  }\n>;\n\nconst inputThemeData: Readonly<InputThemeData> = {\n  light: {\n    textColor: 'text-black',\n    backgroundColor: 'bg-white',\n    iconBorderColor: 'border-[#B9CAD3]',\n    hoverBackgroundColor:\n      '[&:hover>div]:bg-light-secondary/10 [&:active>div]:bg-light-secondary/20'\n  },\n  dim: {\n    textColor: 'text-[#F7F9F9]',\n    backgroundColor: 'bg-[#15202B]',\n    iconBorderColor: 'border-[#5C6E7E]',\n    hoverBackgroundColor:\n      '[&:hover>div]:bg-light-secondary/10 [&:active>div]:bg-light-secondary/20'\n  },\n  dark: {\n    textColor: 'text-dark-primary',\n    backgroundColor: 'bg-black',\n    iconBorderColor: 'border-[#3E4144]',\n    hoverBackgroundColor:\n      '[&:hover>div]:bg-dark-primary/10 [&:active>div]:bg-dark-primary/20'\n  }\n};\n\nexport function InputThemeRadio({\n  type,\n  label\n}: InputThemeRadioProps): JSX.Element {\n  const { theme, changeTheme } = useTheme();\n\n  const { textColor, backgroundColor, iconBorderColor, hoverBackgroundColor } =\n    inputThemeData[type];\n\n  const isChecked = type == theme;\n\n  return (\n    <label\n      className={cn(\n        `flex cursor-pointer items-center gap-2 rounded p-3 font-bold ring-main-accent transition\n         duration-200 [&:has(div>input:checked)]:ring-2`,\n        textColor,\n        backgroundColor,\n        hoverBackgroundColor\n      )}\n      htmlFor={type}\n    >\n      <div className='hover-animation flex h-10 w-10 items-center justify-center rounded-full'>\n        <input\n          className='peer absolute h-0 w-0 opacity-0'\n          id={type}\n          type='radio'\n          name='theme'\n          value={type}\n          checked={isChecked}\n          onChange={changeTheme}\n        />\n        <i\n          className={cn(\n            `flex h-5 w-5 items-center justify-center rounded-full \n             border-2 border-[#B9CAD3] text-white transition\n             duration-200 peer-checked:border-transparent\n             peer-checked:bg-main-accent peer-checked:inner:opacity-100`,\n            iconBorderColor\n          )}\n        >\n          <HeroIcon\n            className='h-full w-full p-0.5 opacity-0 transition-opacity duration-200'\n            iconName='CheckIcon'\n          />\n        </i>\n      </div>\n      {label}\n    </label>\n  );\n}\n"
  },
  {
    "path": "src/components/input/input.tsx",
    "content": "import { UserAvatar } from '@components/user/user-avatar';\nimport { Message } from '@farcaster/hub-web';\nimport { useAuth } from '@lib/context/auth-context';\nimport type { FilesWithId, ImageData, ImagesPreview } from '@lib/types/file';\nimport type { User, UsersMapType } from '@lib/types/user';\nimport { getHttpsUrls, sleep } from '@lib/utils';\nimport { getImagesData } from '@lib/validation';\nimport cn from 'clsx';\nimport type { Variants } from 'framer-motion';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { debounce } from 'lodash';\nimport Link from 'next/link';\nimport type { ChangeEvent, ClipboardEvent, FormEvent, ReactNode } from 'react';\nimport { useCallback, useEffect, useId, useRef, useState } from 'react';\nimport { toast } from 'react-hot-toast';\nimport useSWR from 'swr';\nimport { createCastMessage, submitHubMessage } from '../../lib/farcaster/utils';\nimport { fetchJSON } from '../../lib/fetch';\nimport { uploadToImgur } from '../../lib/imgur/upload';\nimport { BaseResponse } from '../../lib/types/responses';\nimport { TopicResponse, TopicType } from '../../lib/types/topic';\nimport { ExternalEmbed } from '../../lib/types/tweet';\nimport { SearchTopics } from '../search/search-topics';\nimport { UserSearchResult } from '../search/user-search-result';\nimport { TweetEmbed } from '../tweet/tweet-embed';\nimport { TopicView, TweetTopicSkeleton } from '../tweet/tweet-topic';\nimport { Loading } from '../ui/loading';\nimport { ImagePreview } from './image-preview';\nimport { InputForm, fromTop } from './input-form';\nimport { InputOptions } from './input-options';\n\ntype InputProps = {\n  modal?: boolean;\n  reply?: boolean;\n  parent?: { id: string; username: string; userId: string };\n  disabled?: boolean;\n  children?: ReactNode;\n  replyModal?: boolean;\n  parentUrl?: string;\n  closeModal?: () => void;\n};\n\nexport const variants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 }\n};\n\n// TODO: Generalize this and move it somewhere else\nfunction extractAndReplaceMentions(\n  input: string,\n  usersMap: { [key: string]: number }\n) {\n  let result = '';\n  let mentions: number[] = [];\n  let mentionsPositions: number[] = [];\n\n  // Split on newlines and spaces, preserving delimiters\n  let splits = input.split(/(\\s|\\n)/);\n\n  splits.forEach((split, i) => {\n    if (split.startsWith('@')) {\n      const username = split.slice(1);\n\n      // Check if user is in the usersMap\n      if (username in usersMap) {\n        // Get the starting position of each username mention\n        const position = Buffer.from(result).length;\n\n        mentions.push(usersMap[username]);\n        mentionsPositions.push(position);\n\n        // result += '@[...]'; // replace username mention with what you would like\n      } else {\n        result += split;\n      }\n    } else {\n      result += split;\n    }\n  });\n\n  // Return object with replaced text and user mentions array\n  return {\n    text: result,\n    mentions,\n    mentionsPositions\n  };\n}\n\nexport function Input({\n  modal,\n  reply,\n  parent,\n  disabled,\n  children,\n  replyModal,\n  parentUrl,\n  closeModal\n}: InputProps): JSX.Element {\n  const [selectedImages, setSelectedImages] = useState<FilesWithId>([]);\n  const [imagesPreview, setImagesPreview] = useState<ImagesPreview>([]);\n  const [inputValue, setInputValue] = useState('');\n  const [loading, setLoading] = useState(false);\n  const [visited, setVisited] = useState(false);\n\n  const [embedUrls, setEmbedUrls] = useState<string[]>([]); // URLs to be fetched\n  const [embeds, setEmbeds] = useState<ExternalEmbed[]>([]); // Fetched embeds\n  const [ignoredEmbedUrls, setIgnoredEmbedUrls] = useState<string[]>([]); // URLs of embeds to be ignored in the cast message\n\n  const [topicUrl, setTopicUrl] = useState(parentUrl);\n  const [showingTopicSelector, setShowingTopicSelector] = useState(false);\n  const [topic, setTopic] = useState<TopicType | null>();\n\n  const { data: topicResult, isValidating: loadingTopic } = useSWR(\n    topicUrl ? `/api/topic?url=${encodeURIComponent(topicUrl)}` : null,\n    async (url) => {\n      const res = await fetchJSON<TopicResponse>(url);\n      return res.result;\n    },\n    { revalidateOnFocus: false }\n  );\n\n  useEffect(() => {\n    if (topicUrl !== parentUrl) {\n      setTopicUrl(parentUrl);\n    }\n  }, [parentUrl]);\n\n  useEffect(() => {\n    if (topicUrl === topic?.url || topic === undefined) return;\n    setTopicUrl(topic?.url);\n  }, [topic]);\n\n  useEffect(() => {\n    if (topicResult && topic?.url !== topicResult.url) {\n      setTopic(topicResult);\n    }\n  }, [topicResult]);\n\n  const { user, isAdmin } = useAuth();\n  const { name, username, photoURL } = user as User;\n\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n\n  const previewCount = imagesPreview.length;\n  const isUploadingImages = !!previewCount;\n\n  useEffect(\n    () => {\n      if (modal) inputRef.current?.focus();\n      return cleanImage;\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    []\n  );\n\n  const sendTweet = async (): Promise<void> => {\n    inputRef.current?.blur();\n\n    setLoading(true);\n\n    if (!inputValue && selectedImages.length === 0) {\n      setLoading(false);\n      return;\n    }\n\n    const isReplying = reply ?? replyModal;\n\n    const userId = user?.id as string;\n\n    if (isReplying && !parent) {\n      setLoading(false);\n      return;\n    }\n\n    const uploadedLinks: string[] = [];\n\n    // Sequentially upload files\n    for (let i = 0; i < selectedImages.length; i++) {\n      const link = await uploadToImgur(selectedImages[i]);\n\n      if (!link) {\n        toast.error(\n          () => <span className='flex gap-2'>Failed to upload image</span>,\n          { duration: 6000 }\n        );\n        setLoading(false);\n        return;\n      }\n\n      uploadedLinks.push(link);\n    }\n\n    const rawText = inputValue.trim();\n\n    // Get fids of mentioned users\n    const mentionedUsers = ((input: string) => {\n      let splits = input.split(/(\\s|\\n)/);\n\n      const usernames: string[] = [];\n\n      splits.forEach((split, i) => {\n        if (split.startsWith('@')) {\n          const username = split.slice(1);\n          usernames.push(username);\n        }\n      });\n\n      return usernames;\n    })(rawText);\n\n    const usersMapResponse = await fetchJSON<\n      BaseResponse<{ [key: string]: number }>\n    >(`/api/user/resolve-usernames?usernames=${mentionedUsers.join(',')}`);\n    const usersMap = usersMapResponse.result;\n\n    if (!usersMap) {\n      toast.error(\n        () => <span className='flex gap-2'>Failed to resolve usernames</span>,\n        { duration: 6000 }\n      );\n      setLoading(false);\n      return;\n    }\n\n    // Extract mentions for cast message\n    const { text, mentions, mentionsPositions } = extractAndReplaceMentions(\n      rawText,\n      usersMap\n    );\n\n    // setLoading(false);\n    // return;\n\n    // TODO: Limit to only 2 embeds\n    const castMessage = await createCastMessage({\n      text: text,\n      fid: parseInt(userId),\n      embeds: [\n        ...uploadedLinks.map((link) => ({ url: link })),\n        ...embeds\n          .filter((embed) => !ignoredEmbedUrls.includes(embed.url))\n          .map(({ url }) => ({ url }))\n      ],\n      mentions: mentions,\n      mentionsPositions: mentionsPositions,\n      parentCastHash: isReplying && parent ? parent.id : undefined,\n      parentCastFid: isReplying && parent ? parseInt(parent.userId) : undefined,\n      parentUrl: !parent ? topicUrl : undefined\n    });\n\n    if (castMessage) {\n      const res = await submitHubMessage(castMessage);\n      const message = Message.fromJSON(res);\n\n      await sleep(500);\n\n      if (!modal && !replyModal) {\n        discardTweet();\n        setLoading(false);\n      }\n\n      if (closeModal) closeModal();\n\n      const tweetId = Buffer.from(message.hash).toString('hex');\n\n      toast.success(\n        () => (\n          <span className='flex gap-2'>\n            Your post was sent\n            <Link\n              href={`/tweet/${tweetId}`}\n              className='custom-underline font-bold'\n            >\n              View\n            </Link>\n          </span>\n        ),\n        { duration: 6000 }\n      );\n    } else {\n      setLoading(false);\n      toast.error(\n        () => <span className='flex gap-2'>Failed to create post</span>,\n        { duration: 6000 }\n      );\n    }\n  };\n\n  const handleImageUpload = (\n    e: ChangeEvent<HTMLInputElement> | ClipboardEvent<HTMLTextAreaElement>\n  ): void => {\n    const isClipboardEvent = 'clipboardData' in e;\n\n    if (isClipboardEvent) {\n      const isPastingText = e.clipboardData.getData('text');\n      if (isPastingText) return;\n    }\n\n    const files = isClipboardEvent ? e.clipboardData.files : e.target.files;\n\n    const imagesData = getImagesData(files, previewCount);\n\n    if (!imagesData) {\n      toast.error('Please choose a GIF or photo up to 4');\n      return;\n    }\n\n    const { imagesPreviewData, selectedImagesData } = imagesData;\n\n    setImagesPreview([...imagesPreview, ...imagesPreviewData]);\n    setSelectedImages([...selectedImages, ...selectedImagesData]);\n\n    inputRef.current?.focus();\n  };\n\n  const removeImage = (targetId: string) => (): void => {\n    setSelectedImages(selectedImages.filter(({ id }) => id !== targetId));\n    setImagesPreview(imagesPreview.filter(({ id }) => id !== targetId));\n\n    const { src } = imagesPreview.find(\n      ({ id }) => id === targetId\n    ) as ImageData;\n\n    URL.revokeObjectURL(src);\n  };\n\n  const cleanImage = (): void => {\n    imagesPreview.forEach(({ src }) => URL.revokeObjectURL(src));\n\n    setSelectedImages([]);\n    setImagesPreview([]);\n  };\n\n  const discardTweet = (): void => {\n    setInputValue('');\n    setVisited(false);\n    cleanImage();\n    setEmbedUrls([]);\n    setEmbeds([]);\n    setIgnoredEmbedUrls([]);\n\n    inputRef.current?.blur();\n  };\n\n  const handleEmbedsChange = (value: string) => {\n    if (value) {\n      const urls = getHttpsUrls(value).filter(\n        (url) => !ignoredEmbedUrls.includes(url)\n      );\n      setEmbedUrls(urls.slice(0, 2));\n    }\n  };\n\n  const handleChangeDebounced = useCallback(\n    debounce((e) => {\n      handleEmbedsChange(e.target.value);\n    }, 1500),\n    []\n  );\n\n  const [showUsers, setShowUsers] = useState(false);\n  const [searchTerm, setSearchTerm] = useState('');\n\n  const {\n    data: usersSearch,\n    error,\n    isValidating: usersSearchLoading\n  } = useSWR(\n    searchTerm.length > 0 ? `/api/search?q=${searchTerm}` : null,\n    async (url) => (await fetchJSON<BaseResponse<User[]>>(url)).result\n  );\n\n  const debouncedSetSearchTerm = useCallback(\n    debounce((value) => {\n      setSearchTerm(value);\n    }, 1000),\n    []\n  );\n\n  useEffect(() => {\n    if (!inputRef.current) return;\n    const cursorPosition = inputRef.current.selectionStart;\n    const textBeforeCursor = inputValue.slice(0, cursorPosition);\n\n    // TODO: Handle edge cases like \\n\n    const lastKeyword = textBeforeCursor.split(' ').pop() || '';\n\n    if (lastKeyword.startsWith('@')) {\n      setShowUsers(true);\n      debouncedSetSearchTerm(lastKeyword.slice(1));\n    } else {\n      setShowUsers(false);\n    }\n  }, [inputValue]);\n\n  const handleUserClick = (user: User) => {\n    if (!inputRef.current) return;\n    const cursorPosition = inputRef.current.selectionStart;\n    const textBeforeCursor = inputValue.slice(0, cursorPosition);\n    const textAfterCursor = inputValue.slice(cursorPosition);\n\n    const lastSpaceBeforeCursorIndex = textBeforeCursor.lastIndexOf(' ');\n\n    const newTextBeforeCursor = textBeforeCursor.slice(\n      0,\n      lastSpaceBeforeCursorIndex + 1\n    );\n    const newTextAfterCursor = '@' + user.username + ' ' + textAfterCursor;\n\n    setInputValue(newTextBeforeCursor + newTextAfterCursor);\n\n    setShowUsers(false);\n  };\n\n  const handleChange = ({\n    target: { value }\n  }: ChangeEvent<HTMLTextAreaElement>): void => {\n    setInputValue(value);\n  };\n\n  const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {\n    e.preventDefault();\n    void sendTweet();\n  };\n\n  const handleFocus = (): void => setVisited(!loading);\n\n  const formId = useId();\n\n  const inputLimit = 320;\n\n  const inputLength = Buffer.from(inputValue).length;\n  const isValidInput = !!inputValue.trim().length;\n  const isCharLimitExceeded = inputLength > inputLimit;\n\n  const isValidTweet =\n    !isCharLimitExceeded && (isValidInput || isUploadingImages);\n\n  const { data: newEmbeds, isValidating } = useSWR(\n    embedUrls.length > 0 ? `/api/embeds?urls=${embedUrls.join(',')}` : null,\n    fetchJSON<(ExternalEmbed | null)[]>\n  );\n\n  useEffect(() => {\n    setEmbeds((prevEmbeds) => {\n      if (newEmbeds) {\n        return newEmbeds.filter((embed) => embed !== null) as ExternalEmbed[];\n      } else {\n        return prevEmbeds;\n      }\n    });\n  }, [newEmbeds]);\n\n  useEffect(() => {\n    handleEmbedsChange(inputValue);\n  }, [ignoredEmbedUrls]);\n\n  return (\n    <form\n      className={cn('flex flex-col', {\n        '-mx-4': reply,\n        'gap-2': replyModal,\n        'cursor-not-allowed': disabled\n      })}\n      onSubmit={handleSubmit}\n    >\n      {loading && (\n        <motion.i className='h-1 animate-pulse bg-main-accent' {...variants} />\n      )}\n      {children}\n      {reply && visited && (\n        <motion.p\n          className='-mb-2 ml-[75px] mt-2 text-light-secondary dark:text-dark-secondary'\n          {...fromTop}\n        >\n          Replying to{' '}\n          <Link\n            href={`/user/${parent?.username as string}`}\n            className='custom-underline text-main-accent'\n          >\n            {parent?.username as string}\n          </Link>\n        </motion.p>\n      )}\n      <label\n        className={cn(\n          'hover-animation grid w-full grid-cols-[auto,1fr] gap-3 px-4 py-3',\n          reply\n            ? 'pb-1 pt-3'\n            : replyModal\n              ? 'pt-0'\n              : 'border-b-2 border-light-border dark:border-dark-border',\n          (disabled || loading) && 'pointer-events-none opacity-50'\n        )}\n        htmlFor={formId}\n      >\n        <UserAvatar src={photoURL} alt={name} username={username} />\n        <div className='flex w-full flex-col gap-4'>\n          <InputForm\n            modal={modal}\n            reply={reply}\n            formId={formId}\n            visited={visited}\n            loading={loading}\n            inputRef={inputRef}\n            replyModal={replyModal}\n            inputValue={inputValue}\n            isValidTweet={isValidTweet}\n            isUploadingImages={isUploadingImages}\n            sendTweet={sendTweet}\n            handleFocus={handleFocus}\n            discardTweet={discardTweet}\n            handleChange={(e) => {\n              handleChangeDebounced(e);\n              handleChange(e);\n            }}\n            handleImageUpload={handleImageUpload}\n          >\n            {showUsers &&\n              (usersSearchLoading ? (\n                <Loading />\n              ) : (\n                usersSearch &&\n                usersSearch.length > 0 && (\n                  <ul className='menu-container hover-animation mt-1 overflow-hidden rounded-2xl bg-main-background'>\n                    {usersSearch.map((user) => {\n                      return (\n                        <li\n                          key={user.id}\n                          className='cursor-pointer p-2'\n                          onClick={() => handleUserClick(user)}\n                        >\n                          <UserSearchResult user={user} />\n                        </li>\n                      );\n                    })}\n                  </ul>\n                )\n              ))}\n            {isUploadingImages && (\n              <ImagePreview\n                imagesPreview={imagesPreview}\n                previewCount={previewCount}\n                removeImage={!loading ? removeImage : undefined}\n              />\n            )}\n            {embeds?.map(\n              (embed) =>\n                embed &&\n                !ignoredEmbedUrls.includes(embed.url) && (\n                  <div key={embed.url} className='flex items-center gap-2'>\n                    <button\n                      className='text-light-secondary dark:text-dark-secondary'\n                      onClick={() => {\n                        setIgnoredEmbedUrls([...ignoredEmbedUrls, embed.url]);\n                      }}\n                    >\n                      x\n                    </button>\n                    <TweetEmbed {...embed} key={embed.url} />\n                  </div>\n                )\n            )}\n          </InputForm>\n\n          {loadingTopic ? (\n            <div className='w-10'>\n              <TweetTopicSkeleton />\n            </div>\n          ) : showingTopicSelector && !parent ? (\n            <SearchTopics\n              enabled={showingTopicSelector}\n              onSelectRawUrl={setTopicUrl}\n              onSelectTopic={setTopic}\n              setShowing={setShowingTopicSelector}\n            />\n          ) : (\n            topic && (\n              <div\n                className='cursor-pointer text-light-secondary dark:text-dark-secondary'\n                onClick={() => setShowingTopicSelector(true)}\n              >\n                <TopicView topic={topic} />\n              </div>\n            )\n          )}\n\n          <AnimatePresence initial={false}>\n            {(reply ? reply && visited && !loading : !loading) && (\n              <InputOptions\n                reply={reply}\n                modal={modal}\n                inputLimit={inputLimit}\n                inputLength={inputLength}\n                isValidTweet={isValidTweet}\n                isCharLimitExceeded={isCharLimitExceeded}\n                handleImageUpload={handleImageUpload}\n                options={[\n                  {\n                    name: 'Media',\n                    iconName: 'PhotoIcon',\n                    disabled: false\n                  },\n                  {\n                    name: 'Topic',\n                    iconName: 'ChatBubbleBottomCenterTextIcon',\n                    disabled: false,\n                    onClick() {\n                      setShowingTopicSelector(!showingTopicSelector);\n                    }\n                  }\n                ]}\n              />\n            )}\n          </AnimatePresence>\n        </div>\n      </label>\n    </form>\n  );\n}\n"
  },
  {
    "path": "src/components/input/progress-bar.tsx",
    "content": "import cn from 'clsx';\nimport { ToolTip } from '@components/ui/tooltip';\n\ntype ProgressBarProps = {\n  modal?: boolean;\n  inputLimit: number;\n  inputLength: number;\n  isCharLimitExceeded: boolean;\n};\n\nconst baseOffset = [56.5487, 87.9646] as const;\n\nconst circleStyles = [\n  {\n    container: null,\n    viewBox: '0 0 20 20',\n    stroke: 'stroke-main-accent',\n    r: 9\n  },\n  {\n    container: 'scale-150',\n    viewBox: '0 0 30 30',\n    stroke: 'stroke-accent-yellow',\n    r: 14\n  }\n] as const;\n\nexport function ProgressBar({\n  modal,\n  inputLimit,\n  inputLength,\n  isCharLimitExceeded\n}: ProgressBarProps): JSX.Element {\n  const isCloseToLimit = inputLength >= inputLimit - 20;\n  const baseCircle = baseOffset[+isCloseToLimit];\n\n  const inputPercentage = (inputLength / inputLimit) * 100;\n  const circleLength = baseCircle - (baseCircle * inputPercentage) / 100;\n\n  const remainingCharacters = inputLimit - inputLength;\n  const isHittingCharLimit = remainingCharacters <= 0;\n\n  const { container, viewBox, stroke, r } = circleStyles[+isCloseToLimit];\n\n  return (\n    <button\n      className='group relative cursor-pointer outline-none'\n      type='button'\n    >\n      <i\n        className={cn(\n          'flex h-5 w-5 -rotate-90 items-center justify-center transition',\n          container,\n          remainingCharacters <= -10 && 'opacity-0'\n        )}\n      >\n        <svg\n          className='overflow-visible'\n          width='100%'\n          height='100%'\n          viewBox={viewBox}\n        >\n          <circle\n            className='stroke-light-border dark:stroke-dark-border'\n            cx='50%'\n            cy='50%'\n            fill='none'\n            strokeWidth='2'\n            r={r}\n          />\n          <circle\n            className={cn(\n              'transition-colors',\n              isHittingCharLimit ? 'stroke-accent-red' : stroke\n            )}\n            cx='50%'\n            cy='50%'\n            fill='none'\n            strokeWidth='2'\n            r={r}\n            strokeLinecap='round'\n            style={{\n              strokeDashoffset: !isCharLimitExceeded ? circleLength : 0,\n              strokeDasharray: baseCircle\n            }}\n          />\n        </svg>\n      </i>\n      <span\n        className={cn(\n          `absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2\n           scale-50 text-3xl opacity-0 text-light-secondary dark:text-dark-secondary`,\n          {\n            'scale-100 opacity-100 transition': isCloseToLimit,\n            'text-accent-red': isHittingCharLimit\n          }\n        )}\n      >\n        {remainingCharacters}\n      </span>\n      <ToolTip\n        tip={\n          isCharLimitExceeded\n            ? 'You have exceeded the character limit'\n            : `${remainingCharacters} characters remaining`\n        }\n        modal={modal}\n      />\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/input/search-bar.tsx",
    "content": "import cn from 'clsx';\nimport {\n  DetailedHTMLProps,\n  InputHTMLAttributes,\n  KeyboardEvent,\n  useRef\n} from 'react';\nimport { Button } from '../ui/button';\nimport { HeroIcon } from '../ui/hero-icon';\n\nexport function SearchBar({\n  setInputValue,\n  inputValue,\n  className,\n  ...inputProps\n}: DetailedHTMLProps<\n  InputHTMLAttributes<HTMLInputElement>,\n  HTMLInputElement\n> & {\n  setInputValue: (value: string) => void;\n  inputValue: string;\n  className?: string;\n}) {\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const clearInputValue = (focus?: boolean) => (): void => {\n    if (focus) inputRef.current?.focus();\n    else inputRef.current?.blur();\n\n    setInputValue('');\n  };\n\n  const handleEscape = ({ key }: KeyboardEvent<HTMLInputElement>): void => {\n    if (key === 'Escape') clearInputValue()();\n  };\n\n  const blurTimeout = useRef<NodeJS.Timeout>();\n\n  return (\n    <label\n      className={cn(\n        'group flex items-center justify-between gap-4 rounded-full bg-main-search-background px-4 py-2 transition focus-within:bg-main-background focus-within:ring-2 focus-within:ring-main-accent',\n        className\n      )}\n    >\n      <i>\n        <HeroIcon\n          className='h-5 w-5 text-light-secondary transition-colors \n               group-focus-within:text-main-accent dark:text-dark-secondary'\n          iconName='MagnifyingGlassIcon'\n        />\n      </i>\n      <input\n        className='peer flex-1 bg-transparent outline-none \n             placeholder:text-light-secondary dark:placeholder:text-dark-secondary'\n        type='text'\n        placeholder='Search'\n        ref={inputRef}\n        value={inputValue}\n        // onChange={(e) => {\n        //   handleChange(e);\n        //   handleChangeDebounced(e);\n        // }}\n        onKeyUp={handleEscape}\n        {...inputProps}\n      />\n      <Button\n        className={cn(\n          'accent-tab scale-50 bg-main-accent p-1 opacity-0 transition hover:brightness-90 disabled:opacity-0',\n          inputValue &&\n            'focus:scale-100 focus:opacity-100 peer-focus:scale-100 peer-focus:opacity-100'\n        )}\n        onClick={clearInputValue(true)}\n        disabled={!inputValue}\n      >\n        <HeroIcon className='h-3 w-3 stroke-white' iconName='XMarkIcon' />\n      </Button>\n    </label>\n  );\n}\n"
  },
  {
    "path": "src/components/layout/auth-layout.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { useRouter } from 'next/router';\nimport { useAuth } from '@lib/context/auth-context';\nimport { sleep } from '@lib/utils';\nimport { Placeholder } from '@components/common/placeholder';\nimport type { LayoutProps } from './common-layout';\n\nexport function AuthLayout({\n  children,\n  forceLogin\n}: LayoutProps & { forceLogin?: boolean }): JSX.Element {\n  const [pending, setPending] = useState(true);\n\n  const { user, loading } = useAuth();\n  const { replace } = useRouter();\n\n  useEffect(() => {\n    const checkLogin = async (): Promise<void> => {\n      setPending(true);\n\n      if (user && !forceLogin) {\n        await sleep(500);\n        void replace('/home');\n      } else if (!loading) {\n        await sleep(500);\n        setPending(false);\n      }\n    };\n\n    void checkLogin();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [user, loading, forceLogin]);\n\n  if (loading || pending) return <Placeholder />;\n\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "src/components/layout/common-layout.tsx",
    "content": "import { Aside } from '@components/aside/aside';\nimport { Placeholder } from '@components/common/placeholder';\nimport { useRequireAuth } from '@lib/hooks/useRequireAuth';\nimport type { ReactNode } from 'react';\nimport { AsideTrends } from '../aside/trends';\n\nexport type LayoutProps = {\n  children: ReactNode;\n};\n\nexport function ProtectedLayout({ children }: LayoutProps): JSX.Element {\n  const user = useRequireAuth();\n\n  if (!user) return <Placeholder />;\n\n  return <>{children}</>;\n}\n\nexport function HomeLayout({ children }: LayoutProps): JSX.Element {\n  return (\n    <>\n      {children}\n      <Aside>\n        {/* <Suggestions /> */}\n        <AsideTrends />\n      </Aside>\n    </>\n  );\n}\n\nexport function UserLayout({ children }: LayoutProps): JSX.Element {\n  return (\n    <>\n      {children}\n      <Aside>\n        {/* <Suggestions /> */}\n        <></>\n      </Aside>\n      <></>\n    </>\n  );\n}\n\nexport function TrendsLayout({ children }: LayoutProps): JSX.Element {\n  return (\n    <>\n      {children}\n      <Aside>\n        {/* <Suggestions /> */}\n        <></>\n      </Aside>\n    </>\n  );\n}\n\nexport function PeopleLayout({ children }: LayoutProps): JSX.Element {\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "src/components/layout/main-layout.tsx",
    "content": "import { SWRConfig } from 'swr';\nimport { Toaster } from 'react-hot-toast';\nimport { fetchJSON } from '@lib/fetch';\nimport { WindowContextProvider } from '@lib/context/window-context';\nimport { Sidebar } from '@components/sidebar/sidebar';\nimport type { DefaultToastOptions } from 'react-hot-toast';\nimport type { LayoutProps } from './common-layout';\n\nconst toastOptions: DefaultToastOptions = {\n  style: {\n    color: 'white',\n    borderRadius: '4px',\n    backgroundColor: 'rgb(var(--main-accent))'\n  },\n  success: { duration: 4000 }\n};\n\nexport function MainLayout({ children }: LayoutProps): JSX.Element {\n  return (\n    <div className='flex w-full justify-center gap-0 lg:gap-4'>\n      <WindowContextProvider>\n        <Sidebar />\n        <SWRConfig value={{ fetcher: fetchJSON }}>{children}</SWRConfig>\n      </WindowContextProvider>\n      <Toaster\n        position='bottom-center'\n        toastOptions={toastOptions}\n        containerClassName='mb-12 xs:mb-0'\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/layout/user-data-layout.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { UserHeader } from '@components/user/user-header';\nimport { UserContextProvider } from '@lib/context/user-context';\nimport { useRouter } from 'next/router';\nimport useSWR from 'swr';\nimport { fetchJSON } from '../../lib/fetch';\nimport { UserFull, UserFullResponse, UserResponse } from '../../lib/types/user';\nimport type { LayoutProps } from './common-layout';\n\nexport function UserDataLayout({ children }: LayoutProps): JSX.Element {\n  const {\n    query: { id },\n    back\n  } = useRouter();\n\n  const { data: user, isValidating: loading } = useSWR(\n    id ? `/api/user/${id}` : null,\n    async (url) => (await fetchJSON<UserFullResponse>(url)).result,\n    { revalidateOnFocus: false, revalidateOnReconnect: false }\n  );\n\n  return (\n    <UserContextProvider\n      value={{ user: (user as UserFull) || null, loading: !user && loading }}\n    >\n      {!user && !loading && <SEO title='User not found / Opencast' />}\n      <MainContainer>\n        <MainHeader useActionButton action={back}>\n          <UserHeader />\n        </MainHeader>\n        {children}\n      </MainContainer>\n    </UserContextProvider>\n  );\n}\n"
  },
  {
    "path": "src/components/layout/user-follow-layout.tsx",
    "content": "import { motion } from 'framer-motion';\nimport { useUser } from '@lib/context/user-context';\nimport { Loading } from '@components/ui/loading';\nimport { UserNav } from '@components/user/user-nav';\nimport { variants } from '@components/user/user-header';\nimport type { LayoutProps } from './common-layout';\n\nexport function UserFollowLayout({ children }: LayoutProps): JSX.Element {\n  const { user: userData, loading } = useUser();\n\n  return (\n    <>\n      {!userData ? (\n        <motion.section {...variants}>\n          {loading ? (\n            <Loading className='mt-5 w-full' />\n          ) : (\n            <div className='w-full p-8 text-center'>\n              <p className='text-3xl font-bold'>This account doesn’t exist</p>\n              <p className='text-light-secondary dark:text-dark-secondary'>\n                Try searching for another.\n              </p>\n            </div>\n          )}\n        </motion.section>\n      ) : (\n        <>\n          <UserNav follow userId={userData.username} />\n          {children}\n        </>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/layout/user-home-layout.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { Button } from '@components/ui/button';\nimport { FollowButton } from '@components/ui/follow-button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { Loading } from '@components/ui/loading';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { UserDetails } from '@components/user/user-details';\nimport { UserEditProfile } from '@components/user/user-edit-profile';\nimport { variants } from '@components/user/user-header';\nimport { UserHomeAvatar } from '@components/user/user-home-avatar';\nimport { UserNav } from '@components/user/user-nav';\nimport { UserShare } from '@components/user/user-share';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useUser } from '@lib/context/user-context';\nimport { motion } from 'framer-motion';\nimport { useRouter } from 'next/router';\nimport { useState } from 'react';\nimport { TipModal } from '../modal/tip-modal';\nimport type { LayoutProps } from './common-layout';\n\nexport function UserHomeLayout({ children }: LayoutProps): JSX.Element {\n  const { user, isAdmin } = useAuth();\n  const { user: userData, loading } = useUser();\n\n  const {\n    query: { id }\n  } = useRouter();\n\n  const [isTipModalOpen, setIsTipModalOpen] = useState(false);\n\n  const profileData = userData\n    ? { src: userData.photoURL, alt: userData.name }\n    : null;\n\n  const { id: userId } = user ?? {};\n\n  const isOwner = userData?.id === userId;\n\n  return (\n    <>\n      {userData && (\n        <SEO\n          title={`${`${userData.name} (@${userData.username})`} / Opencast`}\n        />\n      )}\n      <TipModal\n        isUserLoading={loading}\n        tipCloseModal={() => setIsTipModalOpen(false)}\n        tipUserOpen={isTipModalOpen}\n        user={userData || undefined}\n        username={userData?.username || '...'}\n      />\n      <motion.section {...variants} exit={undefined}>\n        {loading ? (\n          <Loading className='mt-5' />\n        ) : !userData ? (\n          <>\n            {/* <UserHomeCover /> */}\n            <div className='flex flex-col gap-8'>\n              <div className='relative flex flex-col gap-3 px-4 py-3'>\n                <UserHomeAvatar />\n                <p className='text-xl font-bold'>@{id}</p>\n              </div>\n              <div className='p-8 text-center'>\n                <p className='text-3xl font-bold'>This account doesn’t exist</p>\n                <p className='text-light-secondary dark:text-dark-secondary'>\n                  Try searching for another.\n                </p>\n              </div>\n            </div>\n          </>\n        ) : (\n          <>\n            {/* <UserHomeCover coverData={coverData} /> */}\n            <div className='relative flex flex-col gap-3 px-4 py-3'>\n              <div className='flex justify-between'>\n                <UserHomeAvatar profileData={profileData} />\n                {isOwner ? (\n                  <UserEditProfile />\n                ) : (\n                  <div className='flex gap-2 self-start'>\n                    <UserShare username={userData.username} />\n                    <Button\n                      className='dark-bg-tab group relative border border-light-line-reply p-2\n                                 hover:bg-light-primary/10 active:bg-light-primary/20 dark:border-light-secondary \n                                 dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20'\n                      onClick={() => setIsTipModalOpen(true)}\n                    >\n                      <HeroIcon className='h-5 w-5' iconName='BanknotesIcon' />\n                      <ToolTip tip='Tip' />\n                    </Button>\n                    <FollowButton\n                      userTargetId={userData.id}\n                      userTargetUsername={userData.username}\n                    />\n                    {isAdmin && <UserEditProfile hide />}\n                  </div>\n                )}\n              </div>\n              <UserDetails {...userData} />\n            </div>\n          </>\n        )}\n      </motion.section>\n      {userData && (\n        <>\n          <UserNav userId={userData.username} />\n          {children}\n        </>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/login/login-footer.tsx",
    "content": "// const footerLinks = [\n//   ['About', 'https://about.twitter.com'],\n//   ['Help Center', 'https://help.twitter.com'],\n//   ['Privacy Policy', 'https://twitter.com/tos'],\n//   ['Cookie Policy', 'https://support.twitter.com/articles/20170514'],\n//   ['Accessibility', 'https://help.twitter.com/resources/accessibility'],\n//   [\n//     'Ads Info',\n//     'https://business.twitter.com/en/help/troubleshooting/how-twitter-ads-work.html'\n//   ],\n//   ['Blog', 'https://blog.twitter.com'],\n//   ['Status', 'https://status.twitterstat.us'],\n//   ['Careers', 'https://careers.twitter.com'],\n//   ['Brand Resources', 'https://about.twitter.com/press/brand-assets'],\n//   ['Advertising', 'https://ads.twitter.com/?ref=gl-tw-tw-twitter-advertise'],\n//   ['Marketing', 'https://marketing.twitter.com'],\n//   ['Twitter for Business', 'https://business.twitter.com'],\n//   ['Developers', 'https://developer.twitter.com'],\n//   ['Directory', 'https://twitter.com/i/directory/profiles'],\n//   ['Settings', 'https://twitter.com/settings']\n// ] as const;\n\nexport function LoginFooter(): JSX.Element {\n  return (\n    <footer className='hidden justify-center p-4 text-sm text-light-secondary dark:text-dark-secondary lg:flex'>\n      <nav className='flex flex-wrap justify-center gap-4 gap-y-2'>\n        {/* {footerLinks.map(([linkName, href]) => (\n          <a\n            className='custom-underline'\n            target='_blank'\n            rel='noreferrer'\n            href={href}\n            key={linkName}\n          >\n            {linkName}\n          </a>\n        ))} */}\n        <p>Opencast</p>\n      </nav>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "src/components/login/login-main.tsx",
    "content": "import { Button } from '@components/ui/button';\nimport { CustomIcon } from '@components/ui/custom-icon';\nimport { NextImage } from '@components/ui/next-image';\nimport Link from 'next/link';\nimport { bytesToHex } from 'viem';\nimport { useAuth } from '../../lib/context/auth-context';\nimport { getKeyPair } from '../../lib/crypto';\nimport { useModal } from '../../lib/hooks/useModal';\nimport { addKeyPair } from '../../lib/keys';\nimport WalletSignInModal from '../modal/sign-in-modal-wallet';\nimport { WarpcastSignInModal } from '../modal/sign-in-modal-warpcast';\nimport { HeroIcon } from '../ui/hero-icon';\n\nexport function LoginMain(): JSX.Element {\n  const {\n    openModal: openModalWarpcast,\n    closeModal: closeModalWarpcast,\n    open: openWarpcast\n  } = useModal();\n\n  const {\n    openModal: openModalWallet,\n    closeModal: closeModalWallet,\n    open: openWallet\n  } = useModal();\n\n  const { handleUserAuth } = useAuth();\n\n  return (\n    <main className='grid lg:grid-cols-[1fr,45vw]'>\n      <div className='relative hidden items-center justify-center  lg:flex'>\n        <NextImage\n          imgClassName='object-cover'\n          blurClassName='bg-accent-blue'\n          src='/assets/twitter-banner.png'\n          alt='Opencast banner'\n          layout='fill'\n          useSkeleton\n        />\n        <i className='absolute'>\n          <CustomIcon className='h-96 w-96 text-white' iconName='TwitterIcon' />\n        </i>\n      </div>\n      <WarpcastSignInModal\n        closeModal={closeModalWarpcast}\n        open={openWarpcast}\n      ></WarpcastSignInModal>\n      <WalletSignInModal\n        closeModal={closeModalWallet}\n        open={openWallet}\n      ></WalletSignInModal>\n\n      <div className='flex flex-col items-center justify-between gap-6 p-8 lg:items-start lg:justify-center'>\n        <i className='mb-0 self-center lg:mb-10 lg:self-auto'>\n          <CustomIcon\n            className='-mt-4 h-6 w-6 text-accent-blue lg:h-12 lg:w-12 dark:lg:text-twitter-icon'\n            iconName='TwitterIcon'\n          />\n        </i>\n        <div className='flex max-w-xs flex-col gap-4 font-twitter-chirp-extended lg:max-w-none lg:gap-16'>\n          <h1\n            className='text-3xl before:content-[\"See_what’s_happening_in_the_world_right_now.\"] \n                       lg:text-6xl lg:before:content-[\"Happening_now\"]'\n          />\n          <h2 className='hidden text-xl lg:block lg:text-3xl'>\n            Use Opencast today.\n          </h2>\n        </div>\n        <div className='flex max-w-xs flex-col gap-6 [&_button]:py-2'>\n          <div className='grid gap-3 font-bold'>\n            <Button\n              className='flex justify-center gap-2 border border-light-line-reply font-bold text-light-primary transition\n                         hover:bg-[#e6e6e6] focus-visible:bg-[#e6e6e6] active:bg-[#cccccc] dark:border-0 dark:bg-white\n                         dark:hover:brightness-90 dark:focus-visible:brightness-90 dark:active:brightness-75'\n              onClick={openModalWarpcast}\n            >\n              <CustomIcon iconName='TriangleIcon' /> Sign in with Warpcast\n            </Button>\n            <Button\n              className='flex justify-center gap-2 border border-light-line-reply font-bold text-light-primary transition\n                         hover:bg-[#e6e6e6] focus-visible:bg-[#e6e6e6] active:bg-[#cccccc] dark:border-0 dark:bg-white\n                         dark:hover:brightness-90 dark:focus-visible:brightness-90 dark:active:brightness-75'\n              onClick={openModalWallet}\n            >\n              <HeroIcon iconName='GlobeAltIcon' /> Sign in with Ethereum\n            </Button>\n            <Button\n              className='flex justify-center gap-2 border border-light-line-reply font-bold text-light-primary transition\n                         hover:bg-[#e6e6e6] focus-visible:bg-[#e6e6e6] active:bg-[#cccccc] dark:border-0 dark:bg-white\n                         dark:hover:brightness-90 dark:focus-visible:brightness-90 dark:active:brightness-75'\n              onClick={async () => {\n                const challenge = new Uint8Array(32);\n\n                const assertion = await navigator.credentials.get({\n                  publicKey: {\n                    challenge,\n                    extensions: {\n                      // @ts-ignore -- This is a valid property\n                      largeBlob: {\n                        read: true\n                      }\n                    }\n                  }\n                });\n\n                try {\n                  if (\n                    // @ts-ignore -- This is a valid property\n                    typeof assertion?.getClientExtensionResults().largeBlob\n                      .blob !== 'undefined'\n                  ) {\n                    // Reading a large blob was successful.\n                    const blobBits = new Uint8Array(\n                      // @ts-ignore -- This is a valid property\n                      assertion.getClientExtensionResults().largeBlob.blob\n                    );\n                    const privateKey = bytesToHex(blobBits);\n                    const keyPair = await getKeyPair(privateKey);\n\n                    addKeyPair(keyPair);\n                    handleUserAuth(keyPair);\n                  } else {\n                    // The large blob could not be read (e.g. because the data is corrupted).\n                    // The assertion is still valid.\n                    console.log('The large blob could not be read.');\n                  }\n                } catch (error) {\n                  console.error(error);\n                }\n              }}\n            >\n              <HeroIcon iconName='KeyIcon' /> Sign in with Passkey\n            </Button>\n            <Link\n              href='/home'\n              className='custom-button main-tab flex justify-center gap-2 border border-white bg-black font-bold text-white\n             transition hover:bg-opacity-90 focus-visible:bg-opacity-90 active:bg-opacity-80\n             dark:hover:brightness-125 dark:focus-visible:brightness-125 dark:active:brightness-150'\n            >\n              Continue without signing in\n            </Link>\n            <p\n              className='inner:custom-underline inner:custom-underline text-center text-xs\n                         text-light-secondary inner:text-accent-blue dark:text-dark-secondary'\n            >\n              By signing up you agree that you are doing so at your own risk.\n            </p>\n          </div>\n        </div>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/components/login/sign-in-with-warpcast.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport { toast } from 'react-hot-toast';\nimport QRCode from 'react-qr-code';\nimport { useAuth } from '../../lib/context/auth-context';\nimport { generateKeyPair } from '../../lib/crypto';\nimport { fetchJSON } from '../../lib/fetch';\nimport { KeyPair } from '../../lib/types/keypair';\nimport { BaseResponse } from '../../lib/types/responses';\nimport { addKeyPair } from '../../lib/keys';\n\nconst PENDING_REQUEST_KEY = '-opencast-pendingWarpcastRequest';\n\ntype WarpcastSignerResponseBase = {\n  token: string;\n  deeplinkUrl: string;\n  key: string;\n  state: 'pending' | 'approved' | 'completed';\n  userFid?: number;\n};\n\ntype WarpcastRequest =\n  | (WarpcastSignerResponseBase & {\n      keyPair: KeyPair;\n      authorization: { deadline: number };\n    })\n  | {\n      state: 'preparing';\n      keyPair: KeyPair;\n    };\n\n/* https://warpcast.notion.site/Signer-Request-API-Migration-Guide-Public-9e74827f9070442fb6f2a7ffe7226b3c */\n\nconst WarpcastAuthPopup = ({ closeModal }: { closeModal?: () => void }) => {\n  const [pendingRequest, setPendingRequest] = useState<WarpcastRequest | null>(\n    null\n  );\n  const [polling, setPolling] = useState<boolean>(false);\n  const [deepLinkUrl, setDeepLinkUrl] = useState<string | null>(null);\n  const [initiated, setInitiated] = useState<boolean>(false);\n  const { handleUserAuth } = useAuth();\n\n  useEffect(() => {\n    if (pendingRequest) {\n      localStorage.setItem(PENDING_REQUEST_KEY, JSON.stringify(pendingRequest));\n\n      if (pendingRequest.state === 'preparing') {\n        // Get app signature\n        (\n          fetchJSON(\n            `/api/signer/${pendingRequest!.keyPair.publicKey}/authorize`\n          ) as Promise<\n            BaseResponse<{\n              requestFid: number;\n              signature: string;\n              deadline: number;\n            }>\n          >\n        ).then(({ result: authorizationResult }) => {\n          if (!authorizationResult) {\n            toast.error('Error generating signature');\n            return;\n          } else {\n            // Send signature to Warpcast for transaction broadcast\n            (\n              fetchJSON(`https://api.warpcast.com/v2/signed-key-requests`, {\n                method: 'POST',\n                headers: {\n                  'Content-Type': 'application/json'\n                },\n                body: JSON.stringify({\n                  key: pendingRequest?.keyPair.publicKey,\n                  signature: authorizationResult.signature,\n                  requestFid: authorizationResult.requestFid,\n                  deadline: authorizationResult.deadline\n                })\n              }) as Promise<{\n                result: { signedKeyRequest: WarpcastSignerResponseBase };\n              }>\n            ).then(({ result }) => {\n              if (!result) {\n                toast.error('Error generating signed key');\n                return;\n              }\n\n              setPendingRequest({\n                ...result.signedKeyRequest,\n                keyPair: pendingRequest!.keyPair,\n                authorization: authorizationResult\n              });\n            });\n          }\n        });\n      } else if (pendingRequest.state === 'pending') {\n        setDeepLinkUrl(pendingRequest.deeplinkUrl);\n        if (!polling) {\n          pollForSigner(pendingRequest.token);\n        }\n      } else if (pendingRequest.state === 'completed') {\n        setTimeout(() => {\n          addKeyPair(pendingRequest.keyPair);\n          localStorage.removeItem(PENDING_REQUEST_KEY);\n          handleUserAuth(pendingRequest.keyPair);\n          closeModal?.();\n        }, 5_000); // Give indexer 5 seconds to index event\n      }\n    }\n  }, [pendingRequest]);\n\n  // Initiate the Signer request\n  const initiateSignerRequest = async () => {\n    // Load existing request\n    const pendingWarpcastRequestRaw = localStorage.getItem(\n      PENDING_REQUEST_KEY\n    ) as string;\n\n    let request: WarpcastRequest | undefined;\n\n    if (pendingWarpcastRequestRaw) {\n      const parsed: WarpcastRequest = JSON.parse(pendingWarpcastRequestRaw);\n      // Check that request is still valid\n      if (\n        parsed.state !== 'preparing' &&\n        parsed.authorization.deadline &&\n        parsed.authorization.deadline > Math.floor(Date.now() / 1000)\n      ) {\n        request = parsed;\n      }\n    }\n\n    if (!request) {\n      request = {\n        state: 'preparing',\n        keyPair: await generateKeyPair()\n      };\n      localStorage.setItem(PENDING_REQUEST_KEY, JSON.stringify(request));\n    }\n\n    setPendingRequest(request);\n  };\n\n  // Poll for the status of the Signer request\n  // TODO: Loading indicators\n  const pollForSigner = async (token: string) => {\n    if (pendingRequest?.state !== 'pending') return;\n\n    setPolling(true);\n    let tries = 0;\n    // TODO: Loading indicators\n    while (true || tries < 40) {\n      tries += 1;\n      await new Promise((r) => setTimeout(r, 2000));\n\n      const { result } = (await fetchJSON(\n        `https://api.warpcast.com/v2/signed-key-request?token=${token}`\n      )) as { result: { signedKeyRequest: WarpcastSignerResponseBase } };\n\n      setPendingRequest({\n        ...result.signedKeyRequest,\n        keyPair: pendingRequest.keyPair,\n        authorization: pendingRequest.authorization\n      });\n\n      if (result.signedKeyRequest.state === 'completed') {\n        break;\n      }\n    }\n  };\n\n  useEffect(() => {\n    setInitiated(true);\n  }, []);\n\n  // Debounced\n  useEffect(() => {\n    if (initiated) {\n      initiateSignerRequest();\n    }\n  }, [initiated]);\n\n  return (\n    <div>\n      {deepLinkUrl && (\n        <div>\n          <div className={'rounded bg-white p-2'}>\n            <QRCode value={deepLinkUrl} />{' '}\n          </div>\n          <span className='pt-4 text-gray-500'>\n            On mobile?{' '}\n            <a className='underline' href={deepLinkUrl} target={'_blank'}>\n              Open in Warpcast\n            </a>\n          </span>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default WarpcastAuthPopup;\n"
  },
  {
    "path": "src/components/modal/action-modal.tsx",
    "content": "import { useRef, useEffect } from 'react';\nimport cn from 'clsx';\nimport { Dialog } from '@headlessui/react';\nimport { Button } from '@components/ui/button';\nimport { CustomIcon } from '@components/ui/custom-icon';\n\ntype ActionModalProps = {\n  title: string;\n  useIcon?: boolean;\n  description: string;\n  mainBtnLabel: string;\n  focusOnMainBtn?: boolean;\n  mainBtnClassName?: string;\n  secondaryBtnLabel?: string;\n  secondaryBtnClassName?: string;\n  closeModalBtnLabel?: string;\n  closeModalBtnClassName?: string;\n  action: () => void;\n  secondaryAction?: () => void;\n  closeModal: () => void;\n};\n\nexport function ActionModal({\n  title,\n  useIcon,\n  description,\n  mainBtnLabel,\n  focusOnMainBtn,\n  mainBtnClassName,\n  secondaryBtnLabel,\n  secondaryBtnClassName,\n  closeModalBtnLabel,\n  closeModalBtnClassName,\n  action,\n  secondaryAction,\n  closeModal\n}: ActionModalProps): JSX.Element {\n  const mainBtn = useRef<HTMLButtonElement>(null);\n\n  useEffect(() => {\n    if (!focusOnMainBtn) return;\n    const timeoutId = setTimeout(() => mainBtn.current?.focus(), 50);\n    return () => clearTimeout(timeoutId);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <div className='flex flex-col gap-6'>\n      <div className='flex flex-col gap-4'>\n        {useIcon && (\n          <i className='mx-auto'>\n            <CustomIcon\n              className='h-10 w-10 text-accent-blue dark:text-twitter-icon'\n              iconName='TwitterIcon'\n            />\n          </i>\n        )}\n        <div className='flex flex-col gap-2'>\n          <Dialog.Title className='text-xl font-bold'>{title}</Dialog.Title>\n          <Dialog.Description className='text-light-secondary dark:text-dark-secondary'>\n            {description}\n          </Dialog.Description>\n        </div>\n      </div>\n      <div className='flex flex-col gap-3 inner:py-2 inner:font-bold'>\n        <button\n          className={cn(\n            'custom-button main-tab text-white',\n            mainBtnClassName ??\n              `bg-light-primary hover:bg-light-primary/90 focus-visible:bg-light-primary/90 active:bg-light-primary/80\n               dark:bg-light-border dark:text-light-primary dark:hover:bg-light-border/90\n               dark:focus-visible:bg-light-border/90 dark:active:bg-light-border/75`\n          )}\n          ref={mainBtn}\n          onClick={action}\n        >\n          {mainBtnLabel}\n        </button>\n        {secondaryAction && (\n          <button\n            className={cn(\n              'custom-button main-tab text-white',\n              secondaryBtnClassName ??\n                `bg-light-primary hover:bg-light-primary/90 focus-visible:bg-light-primary/90 active:bg-light-primary/80\n               dark:bg-light-border dark:text-light-primary dark:hover:bg-light-border/90\n               dark:focus-visible:bg-light-border/90 dark:active:bg-light-border/75`\n            )}\n            ref={mainBtn}\n            onClick={secondaryAction}\n          >\n            {secondaryBtnLabel}\n          </button>\n        )}\n        <Button\n          className={cn(\n            'border border-light-line-reply dark:border-light-secondary dark:text-light-border',\n            closeModalBtnClassName ??\n              `hover:bg-light-primary/10 focus-visible:bg-light-primary/10 active:bg-light-primary/20\n               dark:hover:bg-light-border/10 dark:focus-visible:bg-light-border/10 dark:active:bg-light-border/20`\n          )}\n          onClick={closeModal}\n        >\n          {closeModalBtnLabel ?? 'Cancel'}\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/display-modal.tsx",
    "content": "import { UserAvatar } from '@components/user/user-avatar';\nimport { UserName } from '@components/user/user-name';\nimport { InputThemeRadio } from '@components/input/input-theme-radio';\nimport { Button } from '@components/ui/button';\nimport { InputAccentRadio } from '@components/input/input-accent-radio';\nimport type { Theme, Accent } from '@lib/types/theme';\n\ntype DisplayModalProps = {\n  closeModal: () => void;\n};\n\nconst themes: Readonly<[Theme, string][]> = [\n  ['light', 'Default'],\n  ['dim', 'Dim'],\n  ['dark', 'Lights out']\n];\n\nconst accentsColor: Readonly<Accent[]> = [\n  'blue',\n  'yellow',\n  'pink',\n  'purple',\n  'orange',\n  'green'\n];\n\nexport function DisplayModal({ closeModal }: DisplayModalProps): JSX.Element {\n  return (\n    <div className='flex flex-col items-center gap-6'>\n      <div className='flex flex-col gap-3 text-center'>\n        <h2 className='text-2xl font-bold'>Customize your view</h2>\n        <p className='text-light-secondary dark:text-dark-secondary'>\n          These settings affect all the Opencast accounts on this browser.\n        </p>\n      </div>\n      <article\n        className='hover-animation mx-8 rounded-2xl border \n                   border-light-border px-4 py-3 dark:border-dark-border'\n      >\n        <div className='grid grid-cols-[auto,1fr] gap-3'>\n          <UserAvatar src='/assets/twitter-avatar.jpg' alt='Opencast' />\n          <div>\n            <div className='flex gap-1'>\n              <UserName verified name='Opencast' />\n              <p className='text-light-secondary dark:text-dark-secondary'>\n                @opencast\n              </p>\n              <div className='flex gap-1 text-light-secondary dark:text-dark-secondary'>\n                <i>·</i>\n                <p>26m</p>\n              </div>\n            </div>\n            <p className='whitespace-pre-line break-words'>\n              At the heart of Farcaster are short messages called casts — just\n              like this one — which can include photos, videos, links, text,\n              hashtags, and mentions like{' '}\n              <span className='text-main-accent'>@farcaster</span>.\n            </p>\n          </div>\n        </div>\n      </article>\n      <div className='flex w-full flex-col gap-1'>\n        <p className='text-sm font-bold text-light-secondary dark:text-dark-secondary'>\n          Color\n        </p>\n        <div\n          className='hover-animation grid grid-cols-3 grid-rows-2 justify-items-center gap-3 \n                     rounded-2xl bg-main-sidebar-background py-3 xs:grid-cols-6 xs:grid-rows-none'\n        >\n          {accentsColor.map((accentColor) => (\n            <InputAccentRadio type={accentColor} key={accentColor} />\n          ))}\n        </div>\n      </div>\n      <div className='flex w-full flex-col gap-1'>\n        <p className='text-sm font-bold text-light-secondary dark:text-dark-secondary'>\n          Background\n        </p>\n        <div\n          className='hover-animation grid grid-rows-3 gap-3 rounded-2xl bg-main-sidebar-background\n                     px-4 py-3 xs:grid-cols-3 xs:grid-rows-none'\n        >\n          {themes.map(([themeType, label]) => (\n            <InputThemeRadio type={themeType} label={label} key={themeType} />\n          ))}\n        </div>\n      </div>\n      <Button\n        className='bg-main-accent px-4 py-1.5 font-bold\n                   text-white hover:bg-main-accent/90 active:bg-main-accent/75'\n        onClick={closeModal}\n      >\n        Done\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/edit-profile-modal.tsx",
    "content": "import { useRef } from 'react';\nimport cn from 'clsx';\nimport { MainHeader } from '@components/home/main-header';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { NextImage } from '@components/ui/next-image';\nimport { ToolTip } from '@components/ui/tooltip';\nimport type { ReactNode, ChangeEvent } from 'react';\nimport type { User, UserFull } from '@lib/types/user';\n\ntype EditProfileModalProps = Pick<\n  UserFull,\n  'name' | 'photoURL' | 'coverPhotoURL'\n> & {\n  loading: boolean;\n  children: ReactNode;\n  inputNameError: string;\n  editImage: (\n    type: 'cover' | 'profile'\n  ) => ({ target: { files } }: ChangeEvent<HTMLInputElement>) => void;\n  closeModal: () => void;\n  updateData: () => Promise<void>;\n  removeCoverImage: () => void;\n  resetUserEditData: () => void;\n};\n\nexport function EditProfileModal({\n  name,\n  loading,\n  photoURL,\n  children,\n  inputNameError,\n  editImage,\n  closeModal,\n  updateData,\n  resetUserEditData\n}: EditProfileModalProps): JSX.Element {\n  const coverInputFileRef = useRef<HTMLInputElement>(null);\n  const profileInputFileRef = useRef<HTMLInputElement>(null);\n\n  const handleClick = (type: 'cover' | 'profile') => (): void => {\n    if (type === 'cover') coverInputFileRef.current?.click();\n    else profileInputFileRef.current?.click();\n  };\n\n  return (\n    <>\n      <MainHeader\n        useActionButton\n        disableSticky\n        iconName='XMarkIcon'\n        tip='Close'\n        className='absolute flex w-full items-center gap-6 rounded-tl-2xl'\n        title='Edit profile'\n        action={closeModal}\n      >\n        <div className='ml-auto flex items-center gap-3'>\n          <Button\n            className='dark-bg-tab group relative p-2 hover:bg-light-primary/10\n                       active:bg-light-primary/20 dark:hover:bg-dark-primary/10 \n                       dark:active:bg-dark-primary/10'\n            onClick={resetUserEditData}\n            disabled={loading}\n          >\n            <HeroIcon className='h-5 w-5' iconName={'ArrowPathIcon'} />\n            <ToolTip tip='Reset' />\n          </Button>\n          <Button\n            className='bg-light-primary px-4 py-1 font-bold text-white focus-visible:bg-light-primary/90 \n                       enabled:hover:bg-light-primary/90 enabled:active:bg-light-primary/80 disabled:brightness-75\n                       dark:bg-light-border dark:text-light-primary dark:focus-visible:bg-light-border/90\n                       dark:enabled:hover:bg-light-border/90 dark:enabled:active:bg-light-border/75'\n            onClick={updateData}\n            disabled={!!inputNameError}\n            loading={loading}\n          >\n            Save\n          </Button>\n        </div>\n      </MainHeader>\n      <section\n        className={cn(\n          'h-full overflow-y-auto transition-opacity',\n          loading && 'pointer-events-none opacity-50'\n        )}\n      >\n        <div className='group relative mt-[52px] h-36 xs:h-44 sm:h-48'>\n          <input\n            className='hidden'\n            type='file'\n            accept='image/*'\n            ref={coverInputFileRef}\n            onChange={editImage('cover')}\n          />\n          <div className='h-full bg-light-line-reply dark:bg-dark-line-reply' />\n          <div className='absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 gap-4'>\n            <Button\n              className='group/inner relative bg-light-primary/60 p-2 hover:bg-image-preview-hover/50\n                         focus-visible:bg-image-preview-hover/50'\n              onClick={handleClick('cover')}\n            >\n              <HeroIcon\n                className='hover-animation h-6 w-6 text-dark-primary group-hover:text-white'\n                iconName='CameraIcon'\n              />\n              <ToolTip groupInner tip='Add photo' />\n            </Button>\n          </div>\n        </div>\n        <div className='relative flex flex-col gap-6 px-4 py-3'>\n          <div className='mb-8 xs:mb-12 sm:mb-14'>\n            <input\n              className='hidden'\n              type='file'\n              accept='image/*'\n              ref={profileInputFileRef}\n              onChange={editImage('profile')}\n            />\n            <div\n              className='group absolute aspect-square w-24 -translate-y-1/2\n                         overflow-hidden rounded-full xs:w-32 sm:w-36'\n            >\n              <NextImage\n                useSkeleton\n                className='h-full w-full bg-main-background inner:!m-1 inner:rounded-full'\n                imgClassName='rounded-full transition group-hover:brightness-75 duration-200\n                              group-focus-within:brightness-75'\n                src={photoURL}\n                alt={name}\n                layout='fill'\n              />\n              <Button\n                className='group/inner absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2\n                           bg-light-primary/60 p-2 hover:bg-image-preview-hover/50 \n                           focus-visible:bg-image-preview-hover/50'\n                onClick={handleClick('profile')}\n              >\n                <HeroIcon\n                  className='hover-animation h-6 w-6 text-dark-primary group-hover:text-white'\n                  iconName='CameraIcon'\n                />\n                <ToolTip groupInner tip='Add photo' />\n              </Button>\n            </div>\n          </div>\n          {children}\n          <Button\n            className='accent-tab -mx-4 mb-4 flex cursor-not-allowed items-center justify-between rounded-none\n                       py-2 hover:bg-light-primary/10 active:bg-light-primary/20 disabled:brightness-100\n                       dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20'\n          >\n            <span className='mx-2 text-xl'>Switch to professional</span>\n            <i>\n              <HeroIcon\n                className='h-6 w-6 text-light-secondary dark:text-dark-secondary'\n                iconName='ChevronRightIcon'\n              />\n            </i>\n          </Button>\n        </div>\n      </section>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/image-modal.tsx",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport { useState, useEffect } from 'react';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport cn from 'clsx';\nimport { preventBubbling } from '@lib/utils';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { Loading } from '@components/ui/loading';\nimport { backdrop, modal } from './modal';\nimport type { VariantLabels } from 'framer-motion';\nimport type { ImageData } from '@lib/types/file';\nimport type { IconName } from '@components/ui/hero-icon';\n\ntype ImageModalProps = {\n  tweet?: boolean;\n  imageData: ImageData;\n  previewCount: number;\n  selectedIndex?: number;\n  handleNextIndex?: (type: 'prev' | 'next') => () => void;\n};\n\ntype ArrowButton = ['prev' | 'next', string | null, IconName];\n\nconst arrowButtons: Readonly<ArrowButton[]> = [\n  ['prev', null, 'ArrowLeftIcon'],\n  ['next', 'order-1', 'ArrowRightIcon']\n];\n\nexport function ImageModal({\n  tweet,\n  imageData,\n  previewCount,\n  selectedIndex,\n  handleNextIndex\n}: ImageModalProps): JSX.Element {\n  const [indexes, setIndexes] = useState<number[]>([]);\n  const [loading, setLoading] = useState(true);\n\n  const { src, alt } = imageData;\n\n  const requireArrows = handleNextIndex && previewCount > 1;\n\n  useEffect(() => {\n    if (\n      tweet &&\n      selectedIndex !== undefined &&\n      !indexes.includes(selectedIndex)\n    ) {\n      setLoading(true);\n      setIndexes([...indexes, selectedIndex]);\n    }\n\n    const image = new Image();\n    image.src = src;\n    image.onload = (): void => setLoading(false);\n  }, [...(tweet && previewCount > 1 ? [src] : [])]);\n\n  useEffect(() => {\n    if (!requireArrows) return;\n\n    const handleKeyDown = ({ key }: KeyboardEvent): void => {\n      const callback =\n        key === 'ArrowLeft'\n          ? handleNextIndex('prev')\n          : key === 'ArrowRight'\n          ? handleNextIndex('next')\n          : null;\n\n      if (callback) callback();\n    };\n\n    document.addEventListener('keydown', handleKeyDown);\n    return () => document.removeEventListener('keydown', handleKeyDown);\n  }, [handleNextIndex]);\n\n  return (\n    <>\n      {requireArrows &&\n        arrowButtons.map(([name, className, iconName]) => (\n          <Button\n            className={cn(\n              `absolute z-10 hover:bg-light-primary/10 active:bg-light-primary/20\n               dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20`,\n              name === 'prev' ? 'left-2' : 'right-2',\n              className\n            )}\n            onClick={preventBubbling(handleNextIndex(name))}\n            key={name}\n          >\n            <HeroIcon iconName={iconName} />\n          </Button>\n        ))}\n      <AnimatePresence mode='wait'>\n        {loading ? (\n          <motion.div\n            className='mx-auto'\n            {...backdrop}\n            exit={tweet ? (backdrop.exit as VariantLabels) : undefined}\n            transition={{ duration: 0.15 }}\n          >\n            <Loading iconClassName='w-20 h-20' />\n          </motion.div>\n        ) : (\n          <motion.div className='relative mx-auto' {...modal} key={src}>\n            <picture className='group relative flex max-w-3xl'>\n              <source srcSet={src} type='image/*' />\n              <img\n                className='max-h-[75vh] rounded-md object-contain md:max-h-[80vh]'\n                src={src}\n                alt={alt}\n                onClick={preventBubbling()}\n              />\n              <a\n                className='trim-alt accent-tab absolute bottom-0 right-0 mx-2 mb-2 translate-y-4\n                           rounded-md bg-main-background/40 px-2 py-1 text-sm text-light-primary/80 opacity-0\n                           transition hover:bg-main-accent hover:text-white focus-visible:translate-y-0\n                           focus-visible:bg-main-accent focus-visible:text-white focus-visible:opacity-100\n                           group-hover:translate-y-0 group-hover:opacity-100 dark:text-dark-primary/80'\n                href={src}\n                target='_blank'\n                rel='noreferrer'\n                onClick={preventBubbling(null, true)}\n              >\n                {alt}\n              </a>\n            </picture>\n            <a\n              className='custom-underline absolute left-0 -bottom-7 font-medium text-light-primary/80\n                         decoration-transparent underline-offset-2 transition hover:text-light-primary hover:underline\n                         hover:decoration-light-primary focus-visible:text-light-primary dark:text-dark-primary/80 \n                         dark:hover:text-dark-primary dark:hover:decoration-dark-primary dark:focus-visible:text-dark-primary'\n              href={src}\n              target='_blank'\n              rel='noreferrer'\n              onClick={preventBubbling(null, true)}\n            >\n              Open original\n            </a>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/mobile-sidebar-modal.tsx",
    "content": "import { MainHeader } from '@components/home/main-header';\nimport { MobileSidebarLink } from '@components/sidebar/mobile-sidebar-link';\nimport { navLinks, type NavLink } from '@components/sidebar/sidebar';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { NextImage } from '@components/ui/next-image';\nimport { UserAvatar } from '@components/user/user-avatar';\nimport { UserName } from '@components/user/user-name';\nimport { UserUsername } from '@components/user/user-username';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport type { UserFull } from '@lib/types/user';\nimport Link from 'next/link';\nimport { ActionModal } from './action-modal';\nimport { DisplayModal } from './display-modal';\nimport { Modal } from './modal';\nimport { SavePasskeyModal } from './save-passkey-modal';\n\nexport type MobileNavLink = Omit<NavLink, 'canBeHidden'>;\n\nconst bottomNavLinks: Readonly<MobileNavLink[]> = [];\n\ntype Stats = [string, string, number];\n\ntype MobileSidebarModalProps = Pick<\n  UserFull,\n  | 'name'\n  | 'username'\n  | 'verified'\n  | 'photoURL'\n  | 'following'\n  | 'followers'\n  | 'coverPhotoURL'\n> & {\n  closeModal: () => void;\n};\n\nexport function MobileSidebarModal({\n  name,\n  username,\n  verified,\n  photoURL,\n  following,\n  followers,\n  coverPhotoURL,\n  closeModal\n}: MobileSidebarModalProps): JSX.Element {\n  const { signOut, userNotifications, resetNotifications, user } = useAuth();\n\n  const {\n    open: displayOpen,\n    openModal: displayOpenModal,\n    closeModal: displayCloseModal\n  } = useModal();\n\n  const {\n    open: logOutOpen,\n    openModal: logOutOpenModal,\n    closeModal: logOutCloseModal\n  } = useModal();\n\n  const {\n    open: isSavePasskeyModalOpen,\n    openModal: openSavePasskeyModal,\n    closeModal: closeSavePasskeyModal\n  } = useModal();\n\n  const allStats: Readonly<Stats[]> = [\n    ['following', 'Following', following.length],\n    ['followers', 'Followers', followers.length]\n  ];\n\n  const userLink = `/user/${username}`;\n\n  return (\n    <>\n      <Modal\n        className='items-center justify-center xs:flex'\n        modalClassName='max-w-xl bg-main-background w-full p-8 rounded-2xl hover-animation'\n        open={displayOpen}\n        closeModal={displayCloseModal}\n      >\n        <DisplayModal closeModal={displayCloseModal} />\n      </Modal>\n      <Modal\n        modalClassName='max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={isSavePasskeyModalOpen}\n        closeModal={closeSavePasskeyModal}\n      >\n        <SavePasskeyModal\n          closeSavePasskeyModal={closeSavePasskeyModal}\n          user={user}\n        ></SavePasskeyModal>\n      </Modal>\n      <Modal\n        modalClassName='max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={logOutOpen}\n        closeModal={logOutCloseModal}\n      >\n        <ActionModal\n          useIcon\n          focusOnMainBtn\n          title='Log out of Opencast?'\n          description='You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account.'\n          mainBtnLabel='Log out'\n          action={() => {\n            signOut();\n            logOutCloseModal();\n          }}\n          closeModal={logOutCloseModal}\n        />\n      </Modal>\n      <MainHeader\n        useActionButton\n        className='flex flex-row-reverse items-center justify-between'\n        iconName='XMarkIcon'\n        title='Account info'\n        tip='Close'\n        action={closeModal}\n      />\n      <section className='mt-0.5 flex flex-col gap-2 px-4'>\n        {user?.keyPair && (\n          <>\n            <Link\n              href={userLink}\n              className='blur-picture relative h-20 rounded-md'\n            >\n              {coverPhotoURL ? (\n                <NextImage\n                  useSkeleton\n                  imgClassName='rounded-md'\n                  src={coverPhotoURL}\n                  alt={name}\n                  layout='fill'\n                />\n              ) : (\n                <div className='h-full rounded-md bg-light-line-reply dark:bg-dark-line-reply' />\n              )}\n            </Link>\n            <div className='-mt-4 mb-8 ml-2'>\n              <UserAvatar\n                className='absolute -translate-y-1/2 bg-main-background p-1 hover:brightness-100\n                       [&:hover>figure>span]:brightness-75\n                       [&>figure>span]:[transition:200ms]'\n                username={username}\n                src={photoURL}\n                alt={name}\n                size={60}\n              />\n            </div>\n          </>\n        )}\n        <div className='flex flex-col gap-4 rounded-xl bg-main-sidebar-background p-4'>\n          {user?.keyPair && (\n            <>\n              {' '}\n              <div className='flex flex-col'>\n                <UserName\n                  name={name}\n                  username={username}\n                  verified={verified}\n                  className='-mb-1'\n                />\n                <UserUsername username={username} />\n              </div>\n              <div className='text-secondary flex gap-4'>\n                {allStats.map(([id, label, stat]) => (\n                  <Link\n                    href={`${userLink}/${id}`}\n                    key={id}\n                    className='hover-animation flex h-4 items-center gap-1 border-b border-b-transparent \n                             outline-none hover:border-b-light-primary focus-visible:border-b-light-primary\n                             dark:hover:border-b-dark-primary dark:focus-visible:border-b-dark-primary'\n                  >\n                    <p className='font-bold'>{stat}</p>\n                    <p className='text-light-secondary dark:text-dark-secondary'>\n                      {label}\n                    </p>\n                  </Link>\n                ))}\n                <i className='h-0.5 bg-light-line-reply dark:bg-dark-line-reply' />\n              </div>\n            </>\n          )}\n\n          <nav className='flex flex-col'>\n            {user?.keyPair && (\n              <>\n                <MobileSidebarLink\n                  href={`/user/${username}`}\n                  iconName='UserIcon'\n                  linkName='Profile'\n                />\n                <div\n                  onClick={() => {\n                    resetNotifications();\n                  }}\n                >\n                  {userNotifications && (\n                    <div className='absolute ml-6 mt-2 flex h-4 min-w-[16px] items-center rounded-full bg-main-accent text-white'>\n                      <div className='mx-auto px-1 text-xs'>\n                        {userNotifications < 100 ? userNotifications : '99+'}\n                      </div>\n                    </div>\n                  )}\n                  <MobileSidebarLink\n                    href='https://warpcast.com/~/notifications'\n                    iconName='BellIcon'\n                    linkName={`Notifications`}\n                    newTab\n                  />\n                </div>\n              </>\n            )}\n            {navLinks.map((linkData) => (\n              <MobileSidebarLink {...linkData} key={linkData.href} />\n            ))}\n          </nav>\n          <i className='h-0.5 bg-light-line-reply dark:bg-dark-line-reply' />\n          <nav className='flex flex-col'>\n            {bottomNavLinks.map((linkData) => (\n              <MobileSidebarLink bottom {...linkData} key={linkData.href} />\n            ))}\n            <Button\n              className='accent-tab accent-bg-tab flex items-center gap-2 rounded-md p-1.5 font-bold transition\n                         hover:bg-light-primary/10 focus-visible:ring-2 first:focus-visible:ring-[#878a8c] \n                         dark:hover:bg-dark-primary/10 dark:focus-visible:ring-white'\n              onClick={displayOpenModal}\n            >\n              <HeroIcon className='h-5 w-5' iconName='PaintBrushIcon' />\n              Display\n            </Button>\n\n            {user?.keyPair && (\n              <>\n                <Button\n                  className='accent-tab accent-bg-tab flex items-center gap-2 rounded-md p-1.5 font-bold transition\n                         hover:bg-light-primary/10 focus-visible:ring-2 first:focus-visible:ring-[#878a8c] \n                         dark:hover:bg-dark-primary/10 dark:focus-visible:ring-white'\n                  onClick={openSavePasskeyModal}\n                >\n                  <HeroIcon className='h-5 w-5' iconName='KeyIcon' />\n                  Save Signer Key\n                </Button>\n                <Button\n                  className='accent-tab accent-bg-tab flex items-center gap-2 rounded-md p-1.5 font-bold transition\n                         hover:bg-light-primary/10 focus-visible:ring-2 first:focus-visible:ring-[#878a8c] \n                         dark:hover:bg-dark-primary/10 dark:focus-visible:ring-white'\n                  onClick={logOutOpenModal}\n                >\n                  <HeroIcon\n                    className='h-5 w-5'\n                    iconName='ArrowRightOnRectangleIcon'\n                  />\n                  Log out\n                </Button>\n              </>\n            )}\n          </nav>\n        </div>\n        {!user?.keyPair && (\n          <Link\n            href='/login'\n            className='custom-button main-tab accent-tab right-4 mt-4 bg-main-accent text-center text-lg font-bold text-white\n                   outline-none transition hover:brightness-90 active:brightness-75 xl:w-11/12'\n          >\n            <p>Login</p>\n          </Link>\n        )}\n      </section>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/modal.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion';\nimport { Dialog } from '@headlessui/react';\nimport cn from 'clsx';\nimport type { ReactNode } from 'react';\nimport type { Variants } from 'framer-motion';\n\ntype ModalProps = {\n  open: boolean;\n  children: ReactNode;\n  className?: string;\n  modalAnimation?: Variants;\n  modalClassName?: string;\n  closePanelOnClick?: boolean;\n  closeModal: () => void;\n};\n\nconst variants: Variants[] = [\n  {\n    initial: { opacity: 0 },\n    animate: { opacity: 1 },\n    exit: { opacity: 0 }\n  },\n  {\n    initial: { opacity: 0, scale: 0.8 },\n    animate: {\n      opacity: 1,\n      scale: 1,\n      transition: { type: 'spring', duration: 0.5, bounce: 0.4 }\n    },\n    exit: { opacity: 0, scale: 0.8, transition: { duration: 0.15 } }\n  }\n];\n\nexport const [backdrop, modal] = variants;\n\nexport function Modal({\n  open,\n  children,\n  className,\n  modalAnimation,\n  modalClassName,\n  closePanelOnClick,\n  closeModal\n}: ModalProps): JSX.Element {\n  return (\n    <AnimatePresence>\n      {open && (\n        <Dialog\n          className='relative z-50'\n          open={open}\n          onClose={closeModal}\n          static\n        >\n          <motion.div\n            className='hover-animation override-nav fixed inset-0 bg-black/40 dark:bg-[#5B7083]/40'\n            aria-hidden='true'\n            {...backdrop}\n          />\n          <div\n            className={cn(\n              'fixed inset-0 overflow-y-auto p-4',\n              className ?? 'flex items-center justify-center'\n            )}\n          >\n            <Dialog.Panel\n              className={modalClassName}\n              as={motion.div}\n              {...(modalAnimation ?? modal)}\n              onClick={closePanelOnClick ? closeModal : undefined}\n            >\n              {children}\n            </Dialog.Panel>\n          </div>\n        </Dialog>\n      )}\n    </AnimatePresence>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/save-passkey-modal.tsx",
    "content": "import { useState } from 'react';\nimport { ActionModal } from './action-modal';\nimport { createNewPasskey, storeSignerLargeBlob } from '../../lib/passkeys';\nimport { User } from '../../lib/types/user';\nimport { UserWithKey } from '../../lib/context/auth-context';\n\nexport function SavePasskeyModal({\n  user,\n  closeSavePasskeyModal\n}: {\n  user: UserWithKey | null;\n  closeSavePasskeyModal: () => void;\n}) {\n  const [passkeyCreated, setPasskeyCreated] = useState(false);\n\n  return (\n    <ActionModal\n      useIcon\n      focusOnMainBtn\n      title='Save your Signer Key?'\n      description='This will save your Farcaster signer to a passkey which can be used to import your signer on other devices.'\n      mainBtnLabel='Load Passkey'\n      secondaryBtnLabel='Create New Passkey'\n      secondaryAction={\n        !passkeyCreated\n          ? async () => {\n              if (!user?.keyPair) return;\n\n              createNewPasskey({\n                user,\n                onPasskeyCreated: setPasskeyCreated\n              });\n            }\n          : undefined\n      }\n      action={async () => {\n        if (!user?.keyPair) return;\n\n        storeSignerLargeBlob({\n          privateKey: user.keyPair.privateKey,\n          onSignerStored: (created) => {\n            if (created) {\n              closeSavePasskeyModal();\n            }\n          }\n        });\n      }}\n      closeModal={closeSavePasskeyModal}\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/modal/sign-in-modal-wallet.tsx",
    "content": "import { Dialog } from '@headlessui/react';\nimport { ConnectButton } from '@rainbow-me/rainbowkit';\nimport { useEffect, useState } from 'react';\nimport useSWR from 'swr';\nimport { encodeAbiParameters } from 'viem';\nimport {\n  useAccount,\n  useChainId,\n  useSwitchChain,\n  useWaitForTransactionReceipt,\n  useWriteContract\n} from 'wagmi';\nimport { KEY_GATEWAY } from '../../contracts';\nimport { useAuth } from '../../lib/context/auth-context';\nimport { generateKeyPair } from '../../lib/crypto';\nimport { fetchJSON } from '../../lib/fetch';\nimport useFid from '../../lib/hooks/useConnectedWalletFid';\nimport { addKeyPair } from '../../lib/keys';\nimport { AppAuthResponse, AppAuthType } from '../../lib/types/app-auth';\nimport { KeyPair } from '../../lib/types/keypair';\nimport { User, UserResponse } from '../../lib/types/user';\nimport { truncateAddress } from '../../lib/utils';\nimport { Modal } from '../modal/modal';\nimport { Button } from '../ui/button';\nimport { Loading } from '../ui/loading';\nimport { UserAvatar } from '../user/user-avatar';\nimport { UserName } from '../user/user-name';\nimport { UserUsername } from '../user/user-username';\nimport { add } from 'lodash';\n\nconst KEY_METADATA_TYPE_1 = [\n  {\n    components: [\n      {\n        internalType: 'uint256',\n        name: 'requestFid',\n        type: 'uint256'\n      },\n      {\n        internalType: 'address',\n        name: 'requestSigner',\n        type: 'address'\n      },\n      {\n        internalType: 'bytes',\n        name: 'signature',\n        type: 'bytes'\n      },\n      {\n        internalType: 'uint256',\n        name: 'deadline',\n        type: 'uint256'\n      }\n    ],\n    internalType: 'struct SignedKeyRequestValidator.SignedKeyRequestMetadata',\n    name: 'metadata',\n    type: 'tuple'\n  }\n] as const;\n\nconst PENDING_KEY_REQUEST = '-opencast-pendingSignerRequest';\n\ntype KeyRequest =\n  | {\n      keyPair: KeyPair;\n      authorization: AppAuthType;\n      state: 'pending' | 'authorized' | 'completed';\n    }\n  | {\n      state: 'preparing';\n      keyPair: KeyPair;\n    };\n\nconst WalletSignInModal = ({\n  closeModal,\n  open\n}: {\n  closeModal: () => void;\n  open: boolean;\n}) => {\n  const { handleUserAuth } = useAuth();\n  const chainId = useChainId();\n  const { switchChain } = useSwitchChain();\n  const { address } = useAccount();\n  const { data: idOf } = useFid();\n  const { data: user, isValidating: loadingUser } = useSWR<User | null>(\n    idOf ? `/api/user/${idOf}?full=false` : null,\n    async (url) => (await fetchJSON<UserResponse>(url)).result || null,\n    {}\n  );\n  // const { data: appAuth, isValidating: appAuthLoading } =\n  //   useSWR<AppAuthType | null>(\n  //     keypair ? `/api/signer/${keypair.publicKey}/authorize` : null,\n  //     async (url) => (await fetchJSON<AppAuthResponse>(url)).result || null\n  //   );\n\n  const [appAuthLoading, setAppAuthLoading] = useState<boolean>(false);\n\n  const [pendingRequest, setPendingRequest] = useState<KeyRequest | null>(null);\n  const [polling, setPolling] = useState<boolean>(false);\n\n  const {\n    writeContract: addKey,\n    data: addKeyTxHash,\n    isPending: addKeySignPending,\n    isSuccess: addKeySignSuccess,\n    error: addKeyError\n  } = useWriteContract();\n\n  const { isSuccess: isAddKeyTxSuccess, isLoading: isAddKeyTxLoading } =\n    useWaitForTransactionReceipt({ hash: addKeyTxHash });\n\n  useEffect(() => {}, [addKeyTxHash]);\n\n  useEffect(() => {\n    if (pendingRequest) {\n      localStorage.setItem(PENDING_KEY_REQUEST, JSON.stringify(pendingRequest));\n\n      if (pendingRequest?.state === 'preparing') {\n        setAppAuthLoading(true);\n        fetchJSON<AppAuthResponse>(\n          `/api/signer/${pendingRequest.keyPair.publicKey}/authorize`\n        )\n          .then(({ result: authorizationResult, message }) => {\n            if (!authorizationResult) {\n              console.error('Error generating signature', message);\n              return;\n            }\n            setAppAuthLoading(false);\n            setPendingRequest({\n              ...pendingRequest,\n              authorization: authorizationResult,\n              state: 'authorized'\n            });\n          })\n          .catch((e) => {\n            setAppAuthLoading(false);\n          });\n      } else if (pendingRequest.state === 'authorized' && addKeyTxHash) {\n        setPendingRequest({\n          ...pendingRequest,\n          state: 'pending'\n        });\n      } else if (pendingRequest.state === 'pending') {\n        if (!polling) {\n          pollForSigner();\n        }\n      } else if (pendingRequest.state === 'completed') {\n        addKeyPair(pendingRequest.keyPair);\n        localStorage.removeItem(PENDING_KEY_REQUEST);\n        handleUserAuth(pendingRequest.keyPair);\n        closeModal?.();\n      }\n    }\n  }, [pendingRequest, addKeyTxHash]);\n\n  useEffect(() => {\n    // Load existing request\n    const pendingKeyRequest = localStorage.getItem(\n      PENDING_KEY_REQUEST\n    ) as string;\n\n    let request: KeyRequest | undefined;\n\n    if (pendingKeyRequest) {\n      const parsed: KeyRequest = JSON.parse(pendingKeyRequest);\n      // Check that request is still valid\n      if (\n        parsed.state !== 'preparing' &&\n        parsed.authorization.deadline &&\n        parsed.authorization.deadline > Math.floor(Date.now() / 1000)\n      ) {\n        request = parsed;\n      }\n    }\n\n    if (!request) {\n      newKeyPair();\n      return;\n    }\n\n    setPendingRequest(request);\n  }, []);\n\n  const newKeyPair = () => {\n    generateKeyPair().then((keypair) => {\n      setPendingRequest({\n        keyPair: keypair,\n        state: 'preparing'\n      });\n    });\n  };\n\n  const pollForSigner = async () => {\n    if (pendingRequest?.state !== 'pending') return;\n\n    setPolling(true);\n\n    let tries = 0;\n    // TODO: Loading indicators\n    while (true || tries < 40) {\n      tries += 1;\n      await new Promise((r) => setTimeout(r, 2000));\n\n      const { result } = await fetchJSON<UserResponse>(\n        `/api/signer/${pendingRequest.keyPair.publicKey}/user`\n      );\n\n      if (result?.id) {\n        break;\n      }\n    }\n\n    setPolling(false);\n    setPendingRequest({\n      ...pendingRequest,\n      state: 'completed'\n    });\n  };\n\n  return (\n    <Modal\n      className='flex items-start justify-center'\n      modalClassName='bg-main-background rounded-2xl max-w-xl p-4 overflow-hidden flex justify-center'\n      open={open}\n      closeModal={closeModal}\n    >\n      <div>\n        <div className='flex flex-col gap-2'>\n          <div className='flex'>\n            <Dialog.Title className='flex-grow text-xl font-bold'>\n              Sign in with Ethereum Wallet\n            </Dialog.Title>\n            <button onClick={closeModal}>x</button>\n          </div>\n\n          <Dialog.Description className='text-light-secondary dark:text-dark-secondary'>\n            Connect your wallet below to get started.\n          </Dialog.Description>\n        </div>\n        <div className='flex flex-col justify-center gap-4 p-8 pb-4'>\n          <div className={`p-2 pl-0`} data-rk='data-rk'>\n            <ConnectButton\n              chainStatus={'icon'}\n              showBalance={true}\n            ></ConnectButton>\n          </div>\n          {address && (\n            <>\n              {chainId !== 10 && (\n                <div>\n                  <div>Please connect to the Optimism network</div>\n                  <Button\n                    className='accent-tab mt-2 flex items-center justify-center bg-main-accent font-bold text-white enabled:hover:bg-main-accent/90 enabled:active:bg-main-accent/75'\n                    onClick={() => switchChain({ chainId: 10 })}\n                  >\n                    Switch to Optimism\n                  </Button>\n                </div>\n              )}\n              {!idOf && chainId === 10 && (\n                <div>\n                  This address is not registered on the Farcaster network.\n                </div>\n              )}\n              {idOf &&\n                chainId === 10 &&\n                (loadingUser || appAuthLoading ? (\n                  <Loading />\n                ) : (\n                  user &&\n                  pendingRequest?.state === 'authorized' && (\n                    <div>\n                      <div className='flex flex-wrap items-center gap-2'>\n                        <div className='flex flex-grow gap-3 truncate'>\n                          <UserAvatar\n                            src={user.photoURL}\n                            alt={user.name}\n                            size={40}\n                          />\n                          <div className='hidden truncate text-start leading-5 xl:block'>\n                            <UserName\n                              name={user.name}\n                              className='start'\n                              verified={user.verified}\n                            />\n                            <UserUsername\n                              username={user.username}\n                              disableLink\n                            />\n                          </div>\n                        </div>\n                        {addKeySignPending || isAddKeyTxLoading ? (\n                          <Loading></Loading>\n                        ) : (\n                          !isAddKeyTxSuccess &&\n                          pendingRequest.state === 'authorized' &&\n                          pendingRequest?.keyPair && (\n                            <Button\n                              disabled={addKeySignPending}\n                              onClick={() => {\n                                addKey({\n                                  ...KEY_GATEWAY,\n                                  chainId: 10,\n                                  functionName: 'add',\n                                  args: [\n                                    1,\n                                    pendingRequest.keyPair.publicKey,\n                                    1,\n                                    encodeAbiParameters(KEY_METADATA_TYPE_1, [\n                                      {\n                                        requestFid: BigInt(\n                                          pendingRequest.authorization\n                                            .requestFid\n                                        ),\n                                        requestSigner: pendingRequest\n                                          .authorization\n                                          .requestSigner as `0x${string}`,\n                                        signature: pendingRequest.authorization\n                                          .signature as `0x${string}`,\n                                        deadline: BigInt(\n                                          pendingRequest.authorization.deadline\n                                        )\n                                      }\n                                    ])\n                                  ]\n                                });\n                              }}\n                              className='accent-tab flex-grow items-center justify-center bg-main-accent font-bold text-white enabled:hover:bg-main-accent/90 enabled:active:bg-main-accent/75'\n                            >\n                              Sign in\n                            </Button>\n                          )\n                        )}\n                      </div>\n                      {pendingRequest?.keyPair && (\n                        <div className='mt-2 break-all text-center text-sm text-light-secondary dark:text-dark-secondary'>\n                          Authorizing{' '}\n                          <span title={pendingRequest.keyPair.publicKey}>\n                            {truncateAddress(pendingRequest.keyPair.publicKey)}{' '}\n                          </span>\n                          <button className='underline' onClick={newKeyPair}>\n                            Reset\n                          </button>\n                        </div>\n                      )}\n                      {addKeyTxHash && (\n                        <a\n                          href={`https://optimistic.etherscan.io/tx/${addKeyTxHash}`}\n                          target='_blank'\n                          rel='noopener noreferrer'\n                          className='text-center text-sm text-light-secondary underline dark:text-dark-secondary'\n                        >\n                          View transaction\n                        </a>\n                      )}\n                    </div>\n                  )\n                ))}\n            </>\n          )}\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default WalletSignInModal;\n"
  },
  {
    "path": "src/components/modal/sign-in-modal-warpcast.tsx",
    "content": "import { Dialog } from '@headlessui/react';\nimport WarpcastAuthPopup from '../login/sign-in-with-warpcast';\nimport { Modal } from './modal';\n\nexport function WarpcastSignInModal({\n  open,\n  closeModal\n}: {\n  open: boolean;\n  closeModal: () => void;\n}) {\n  return (\n    <Modal\n      className='flex items-start justify-center'\n      modalClassName='bg-main-background rounded-2xl max-w-xl p-4 overflow-hidden flex justify-center'\n      open={open}\n      closeModal={closeModal}\n    >\n      <div>\n        <div className='flex flex-col gap-2'>\n          <div className='flex'>\n            <Dialog.Title className='flex-grow text-xl font-bold'>\n              Sign in with Warpcast\n            </Dialog.Title>\n            <button onClick={closeModal}>x</button>\n          </div>\n\n          <Dialog.Description className='text-light-secondary dark:text-dark-secondary'>\n            Scan the QR code with the camera app on your device with Warpcast\n            installed.\n          </Dialog.Description>\n        </div>\n        <div className='flex justify-center p-8'>\n          <WarpcastAuthPopup closeModal={closeModal}></WarpcastAuthPopup>\n        </div>\n      </div>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/tip-modal.tsx",
    "content": "import { Dialog } from '@headlessui/react';\nimport { ConnectButton } from '@rainbow-me/rainbowkit';\nimport Link from 'next/link';\nimport { useEffect, useState } from 'react';\nimport { toast } from 'react-hot-toast';\nimport { parseEther } from 'viem';\nimport { useAccount, useChainId, useSendTransaction } from 'wagmi';\nimport * as chains from 'wagmi/chains';\nimport { UserFull } from '../../lib/types/user';\nimport { truncateAddress } from '../../lib/utils';\nimport { Button } from '../ui/button';\nimport { HeroIcon } from '../ui/hero-icon';\nimport { Loading } from '../ui/loading';\nimport { Modal } from './modal';\n\ninterface TipModalProps {\n  tipUserOpen: boolean;\n  tipCloseModal: () => void;\n  isUserLoading: boolean;\n  user?: UserFull;\n  username: string;\n}\n\nexport function TipModal({\n  tipCloseModal,\n  tipUserOpen,\n  isUserLoading,\n  user,\n  username\n}: TipModalProps) {\n  const { address: currentUserAddress } = useAccount();\n  const chainId = useChainId();\n\n  const [tipAmount, setTipAmount] = useState<number>(0.001);\n  const {\n    data: tipTxHash,\n    isPending: tipTxResultLoading,\n    isSuccess: tipTxSuccess,\n    sendTransaction: sendTipTx\n  } = useSendTransaction();\n\n  useEffect(() => {\n    if (!tipTxSuccess) return;\n    tipCloseModal();\n\n    const chainById = Object.values(chains).reduce(\n      (acc: { [key: string]: chains.Chain }, cur) => {\n        if (cur.id) acc[cur.id] = cur;\n        return acc;\n      },\n      {}\n    );\n\n    const chain = chainById[chainId];\n    const explorerUrl = chain?.blockExplorers?.default;\n    const url = `${explorerUrl?.url}/tx/${tipTxHash}`;\n\n    if (!url) return;\n\n    toast.success(\n      () => (\n        <span className='flex gap-2'>\n          Your tip was sent\n          <Link\n            href={`${explorerUrl?.url}/tx/${tipTxHash}`}\n            className='custom-underline font-bold'\n            target='_blank'\n          >\n            View\n          </Link>\n        </span>\n      ),\n      { duration: 6000 }\n    );\n  }, [tipTxSuccess]);\n\n  return (\n    <Modal\n      modalClassName='max-w-sm bg-main-background w-full p-8 rounded-2xl'\n      open={tipUserOpen}\n      closeModal={tipCloseModal}\n    >\n      <div className='flex flex-col gap-6'>\n        <div className='flex flex-col gap-4'>\n          <div className='flex flex-col gap-2'>\n            <div className='flex items-center'>\n              <i className='inline pr-2'>\n                <HeroIcon iconName='BanknotesIcon' />\n              </i>\n              <Dialog.Title className='inline text-xl font-bold'>\n                Tip user\n              </Dialog.Title>\n            </div>\n            <Dialog.Description className='text-light-secondary dark:text-dark-secondary'>\n              Send @{username} some ETH\n            </Dialog.Description>\n          </div>\n          {isUserLoading ? (\n            <Loading />\n          ) : (\n            <div className='flex flex-col'>\n              <div className={`p-2 pl-0`} data-rk='data-rk'>\n                <ConnectButton></ConnectButton>\n              </div>\n              {user?.address ? (\n                currentUserAddress && (\n                  <div className='mt-4 flex flex-col gap-4'>\n                    <div className='flex justify-center gap-2'>\n                      {[0.0005, 0.001, 0.002].map((amount) => (\n                        <button\n                          key={amount}\n                          onClick={() => setTipAmount(amount)}\n                          className={`rounded-full  p-2 \n                       ${\n                         tipAmount === amount\n                           ? 'border-2 border-main-accent font-bold text-main-accent'\n                           : 'border border-gray-500 text-gray-500'\n                       }`}\n                        >\n                          {amount}\n                        </button>\n                      ))}\n                    </div>\n                    {!tipTxResultLoading && user.address ? (\n                      <Button\n                        className='accent-tab mt-2 flex items-center justify-center bg-main-accent font-bold text-white enabled:hover:bg-main-accent/90 enabled:active:bg-main-accent/75'\n                        onClick={() => {\n                          sendTipTx({\n                            to: user?.address as `0x${string}`,\n                            value: parseEther(tipAmount.toString())\n                          });\n                        }}\n                        disabled={tipTxResultLoading || tipAmount === 0}\n                      >\n                        Send{' '}\n                        {tipAmount.toLocaleString(undefined, {\n                          maximumFractionDigits: 6\n                        })}{' '}\n                        ETH\n                      </Button>\n                    ) : (\n                      <Loading></Loading>\n                    )}\n                    <div className='w-full text-center text-gray-500'>\n                      to @{user.username}{' '}\n                      <span title={user.address}>\n                        ({truncateAddress(user.address)})\n                      </span>\n                    </div>\n                  </div>\n                )\n              ) : (\n                <div>User doesn't have an address connected</div>\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/tweet-reply-modal.tsx",
    "content": "import { Input } from '@components/input/input';\nimport { Tweet } from '@components/tweet/tweet';\nimport type { TweetProps } from '@components/tweet/tweet';\n\ntype TweetReplyModalProps = {\n  tweet: TweetProps;\n  closeModal: () => void;\n};\n\nexport function TweetReplyModal({\n  tweet,\n  closeModal\n}: TweetReplyModalProps): JSX.Element {\n  return (\n    <Input\n      modal\n      replyModal\n      parent={{\n        id: tweet.id,\n        username: tweet.user.username,\n        userId: tweet.user.id\n      }}\n      closeModal={closeModal}\n      parentUrl={tweet.topicUrl || undefined}\n    >\n      <Tweet modal parentTweet {...tweet} />\n    </Input>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/tweet-stats-modal.tsx",
    "content": "import { MainHeader } from '@components/home/main-header';\nimport type { ReactNode } from 'react';\nimport type { StatsType } from '@components/view/view-tweet-stats';\n\ntype TweetStatsModalProps = {\n  children: ReactNode;\n  statsType: StatsType | null;\n  handleClose: () => void;\n};\n\nexport function TweetStatsModal({\n  children,\n  statsType,\n  handleClose\n}: TweetStatsModalProps): JSX.Element {\n  return (\n    <>\n      <MainHeader\n        useActionButton\n        disableSticky\n        tip='Close'\n        iconName='XMarkIcon'\n        className='absolute flex w-full items-center gap-6 rounded-tl-2xl'\n        title={`${statsType === 'likes' ? 'Liked' : 'Recasted'} by`}\n        action={handleClose}\n      />\n      {children}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/modal/username-modal.tsx",
    "content": "import { Dialog } from '@headlessui/react';\nimport { CustomIcon } from '@components/ui/custom-icon';\nimport { Button } from '@components/ui/button';\nimport type { ReactNode, FormEvent } from 'react';\n\ntype UsernameModalProps = {\n  loading: boolean;\n  children: ReactNode;\n  available: boolean;\n  alreadySet: boolean;\n  changeUsername: (e: FormEvent<HTMLFormElement>) => Promise<void>;\n  cancelUpdateUsername: () => void;\n};\n\nconst usernameModalData = [\n  {\n    title: 'What should we call you?',\n    description: 'Your @username is unique. You can always change it later.',\n    cancelLabel: 'Skip'\n  },\n  {\n    title: 'Change your username?',\n    description:\n      'Your @username is unique. You can always change it here again.',\n    cancelLabel: 'Cancel'\n  }\n] as const;\n\nexport function UsernameModal({\n  loading,\n  children,\n  available,\n  alreadySet,\n  changeUsername,\n  cancelUpdateUsername\n}: UsernameModalProps): JSX.Element {\n  const { title, description, cancelLabel } = usernameModalData[+alreadySet];\n\n  return (\n    <form\n      className='flex h-full flex-col justify-between'\n      onSubmit={changeUsername}\n    >\n      <div className='flex flex-col gap-6'>\n        <div className='flex flex-col gap-4'>\n          <i className='mx-auto'>\n            <CustomIcon className='h-10 w-10' iconName='TwitterIcon' />\n          </i>\n          <div className='flex flex-col gap-2'>\n            <Dialog.Title className='text-2xl font-bold xs:text-3xl sm:text-4xl'>\n              {title}\n            </Dialog.Title>\n            <Dialog.Description className='text-light-secondary dark:text-dark-secondary'>\n              {description}\n            </Dialog.Description>\n          </div>\n        </div>\n        {children}\n      </div>\n      <div className='flex flex-col gap-3 inner:py-2 inner:font-bold'>\n        <Button\n          className='bg-light-primary text-white transition focus-visible:bg-light-primary/90\n                     enabled:hover:bg-light-primary/90 enabled:active:bg-light-primary/80 \n                     dark:bg-light-border dark:text-light-primary dark:focus-visible:bg-light-border/90 \n                     dark:enabled:hover:bg-light-border/90 dark:enabled:active:bg-light-border/75'\n          type='submit'\n          loading={loading}\n          disabled={!available}\n        >\n          Set username\n        </Button>\n        <Button\n          className='border border-light-line-reply hover:bg-light-primary/10 focus-visible:bg-light-primary/10\n                     active:bg-light-primary/20 dark:border-light-secondary dark:text-light-border \n                     dark:hover:bg-light-border/10 dark:focus-visible:bg-light-border/10 \n                     dark:active:bg-light-border/20'\n          onClick={cancelUpdateUsername}\n        >\n          {cancelLabel}\n        </Button>\n      </div>\n    </form>\n  );\n}\n"
  },
  {
    "path": "src/components/search/search-topics.tsx",
    "content": "import { useState } from 'react';\nimport useSWR from 'swr';\nimport isURL from 'validator/lib/isURL';\nimport { fetchJSON } from '../../lib/fetch';\nimport { TopicType } from '../../lib/types/topic';\nimport { TrendsResponse } from '../../lib/types/trends';\nimport { SearchBar } from '../input/search-bar';\nimport { TopicView } from '../tweet/tweet-topic';\nimport { Loading } from '../ui/loading';\n\nexport function SearchTopics({\n  onSelectRawUrl,\n  onSelectTopic,\n  setShowing,\n  enabled = false\n}: {\n  onSelectRawUrl: (topicUrl: string) => void;\n  onSelectTopic: (topic: TopicType) => void;\n  setShowing: (showing: boolean) => void;\n  enabled: boolean;\n}) {\n  const [topicQuery, setTopicQuery] = useState('');\n\n  const { data: allTopics, isValidating: loadingAllTopics } = useSWR(\n    enabled ? `/api/trends?limit=50` : null,\n    async (url) => {\n      const res = await fetchJSON<TrendsResponse>(url);\n      return res.result;\n    },\n    { revalidateOnFocus: false }\n  );\n\n  return (\n    <div>\n      <SearchBar\n        placeholder='Search topics or paste a link'\n        inputValue={topicQuery}\n        setInputValue={setTopicQuery}\n        onChange={(e) => setTopicQuery(e.target.value)}\n      />\n\n      <div>\n        {loadingAllTopics && <Loading></Loading>}\n        {isURL(topicQuery) && (\n          <div\n            onClick={() => {\n              onSelectRawUrl(topicQuery);\n              setTopicQuery('');\n              setShowing(false);\n            }}\n            className='mt-2 cursor-pointer rounded-lg p-2 text-light-secondary hover:bg-main-accent/10 dark:text-dark-secondary'\n          >\n            Choose \"{topicQuery}\"\n          </div>\n        )}\n        <div className='mt-2 flex flex-wrap gap-2'>\n          {allTopics\n            ?.filter(\n              ({ topic }) =>\n                topic !== null &&\n                (topicQuery.length === 0 ||\n                  topic?.name\n                    .toLowerCase()\n                    .includes(topicQuery.toLowerCase()) ||\n                  topic?.url.toLowerCase().includes(topicQuery.toLowerCase()))\n            )\n            .slice(0, 5)\n            .map(({ topic }, i) => (\n              <div\n                onClick={() => {\n                  onSelectTopic(topic as TopicType);\n                  setTopicQuery('');\n                  setShowing(false);\n                }}\n                key={i}\n                className='cursor-pointer rounded-lg p-2 text-light-secondary hover:bg-main-accent/10 dark:text-dark-secondary'\n              >\n                <TopicView topic={topic!} key={i} />\n              </div>\n            ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/search/user-search-result.tsx",
    "content": "import { User } from '../../lib/types/user';\nimport { NextImage } from '../ui/next-image';\n\nexport function UserSearchResult({\n  user,\n  callback\n}: {\n  user: User;\n  callback?: () => void;\n}) {\n  const { id, username, photoURL, name, verified } = user;\n  return (\n    <div\n      key={id}\n      className='flex w-full cursor-pointer p-3 hover:bg-accent-blue/10 focus-visible:bg-accent-blue/20'\n      onClick={callback}\n    >\n      <NextImage\n        useSkeleton\n        imgClassName='rounded-full'\n        width={48}\n        height={48}\n        src={photoURL}\n        alt={username}\n        key={photoURL}\n      />\n      <div className='flex flex-col pl-2'>\n        <span className='text-light-primary dark:text-dark-primary'>\n          {name}\n        </span>\n        <span className='truncate text-light-secondary dark:text-dark-secondary'>\n          @{username}\n        </span>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/sidebar/menu-link.tsx",
    "content": "import { forwardRef } from 'react';\nimport Link from 'next/link';\nimport type { ComponentPropsWithRef } from 'react';\n\ntype MenuLinkProps = ComponentPropsWithRef<'a'> & {\n  href: string;\n};\n\nexport const MenuLink = forwardRef<HTMLAnchorElement, MenuLinkProps>(\n  ({ href, children, ...rest }, ref) => (\n    <Link href={href} ref={ref} {...rest}>\n      {children}\n    </Link>\n  )\n);\n"
  },
  {
    "path": "src/components/sidebar/mobile-sidebar-link.tsx",
    "content": "import Link from 'next/link';\nimport cn from 'clsx';\nimport { preventBubbling } from '@lib/utils';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport type { MobileNavLink } from '@components/modal/mobile-sidebar-modal';\n\ntype MobileSidebarLinkProps = MobileNavLink & {\n  bottom?: boolean;\n};\n\nexport function MobileSidebarLink({\n  href,\n  bottom,\n  linkName,\n  iconName,\n  disabled,\n  newTab\n}: MobileSidebarLinkProps): JSX.Element {\n  return (\n    <Link\n      href={href}\n      key={href}\n      className={cn(\n        `custom-button accent-tab accent-bg-tab flex items-center rounded-md font-bold \n           transition hover:bg-light-primary/10 focus-visible:ring-2 first:focus-visible:ring-[#878a8c]\n           dark:hover:bg-dark-primary/10 dark:focus-visible:ring-white`,\n        bottom ? 'gap-2 p-1.5 text-base' : 'gap-4 p-2 text-xl',\n        disabled && 'cursor-not-allowed'\n      )}\n      onClick={disabled ? preventBubbling() : undefined}\n      target={newTab ? '_blank' : undefined}\n    >\n      <HeroIcon\n        className={bottom ? 'h-5 w-5' : 'h-7 w-7'}\n        iconName={iconName}\n      />\n      {linkName}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/sidebar/mobile-sidebar.tsx",
    "content": "import { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport { Button } from '@components/ui/button';\nimport { Modal } from '@components/modal/modal';\nimport { MobileSidebarModal } from '@components/modal/mobile-sidebar-modal';\nimport { UserAvatar } from '@components/user/user-avatar';\nimport type { Variants } from 'framer-motion';\nimport type { User, UserFull } from '@lib/types/user';\nimport { HeroIcon } from '../ui/hero-icon';\n\nconst variant: Variants = {\n  initial: { x: '-100%', opacity: 0.8 },\n  animate: {\n    x: -8,\n    opacity: 1,\n    transition: { type: 'spring', duration: 0.8 }\n  },\n  exit: { x: '-100%', opacity: 0.8, transition: { duration: 0.4 } }\n};\n\nexport function MobileSidebar(): JSX.Element {\n  const { user } = useAuth();\n\n  const { photoURL, name } = user!;\n\n  const { open, openModal, closeModal } = useModal();\n\n  return (\n    <>\n      <Modal\n        className='p-0'\n        modalAnimation={variant}\n        modalClassName='pb-4 pl-2 min-h-screen w-72 bg-main-background'\n        open={open}\n        closeModal={closeModal}\n      >\n        <MobileSidebarModal {...user!} closeModal={closeModal} />\n      </Modal>\n      <Button className='accent-tab p-0 xs:hidden' onClick={openModal}>\n        {user?.keyPair ? (\n          <UserAvatar src={photoURL} alt={name} size={30} />\n        ) : (\n          <div className='py-2'>\n            <HeroIcon className={'h-7 w-7'} iconName={'UserIcon'} />\n          </div>\n        )}\n      </Button>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/sidebar/more-settings.tsx",
    "content": "import { DisplayModal } from '@components/modal/display-modal';\nimport { Modal } from '@components/modal/modal';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { Menu } from '@headlessui/react';\nimport { useModal } from '@lib/hooks/useModal';\nimport { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit';\nimport cn from 'clsx';\nimport type { Variants } from 'framer-motion';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { useAccount } from 'wagmi';\n\nexport const variants: Variants = {\n  initial: { opacity: 0, y: 50 },\n  animate: {\n    opacity: 1,\n    y: 0,\n    transition: { type: 'spring', duration: 0.4 }\n  },\n  exit: { opacity: 0, y: 50, transition: { duration: 0.2 } }\n};\n\nexport function MoreSettings(): JSX.Element {\n  const { open, openModal, closeModal } = useModal();\n  const { address } = useAccount();\n  const { openConnectModal } = useConnectModal();\n\n  return (\n    <>\n      <Modal\n        modalClassName='max-w-xl bg-main-background w-full p-8 rounded-2xl hover-animation'\n        open={open}\n        closeModal={closeModal}\n      >\n        <DisplayModal closeModal={closeModal} />\n      </Modal>\n      <Menu className='relative' as='div'>\n        {({ open }): JSX.Element => (\n          <>\n            <Menu.Button className='group relative flex w-full py-1 outline-none'>\n              <div\n                className={cn(\n                  `custom-button flex gap-4 text-xl transition group-hover:bg-light-primary/10 group-focus-visible:ring-2\n                   group-focus-visible:ring-[#878a8c] dark:group-hover:bg-dark-primary/10 dark:group-focus-visible:ring-white\n                   xl:pr-5`,\n                  open && 'bg-light-primary/10 dark:bg-dark-primary/10'\n                )}\n              >\n                <HeroIcon\n                  className='h-7 w-7'\n                  iconName='EllipsisHorizontalCircleIcon'\n                />{' '}\n                <p className='hidden xl:block'>More</p>\n              </div>\n            </Menu.Button>\n            <AnimatePresence>\n              {open && (\n                <Menu.Items\n                  className='menu-container absolute w-60 font-medium xl:w-11/12'\n                  as={motion.div}\n                  {...variants}\n                  static\n                >\n                  <Menu.Item>\n                    {({ active }): JSX.Element => (\n                      <Button\n                        className={cn(\n                          'flex w-full gap-3 rounded-none rounded-b-md p-4 duration-200',\n                          active && 'bg-main-sidebar-background'\n                        )}\n                        onClick={openModal}\n                      >\n                        <HeroIcon iconName='PaintBrushIcon' />\n                        Display\n                      </Button>\n                    )}\n                  </Menu.Item>\n                  {address ? (\n                    <div className='align-center flex w-full items-center gap-3 rounded-none rounded-b-md p-4 '>\n                      <HeroIcon\n                        className='hidden h-6 w-6 xl:block'\n                        iconName='WalletIcon'\n                      />\n                      <ConnectButton\n                        showBalance={false}\n                        accountStatus={'address'}\n                        chainStatus={'none'}\n                      ></ConnectButton>\n                    </div>\n                  ) : (\n                    <Menu.Item>\n                      {({ active }): JSX.Element => (\n                        <Button\n                          className={cn(\n                            'flex w-full gap-3 rounded-none rounded-b-md p-4 duration-200',\n                            active && 'bg-main-sidebar-background'\n                          )}\n                          onClick={openConnectModal}\n                        >\n                          <HeroIcon iconName='WalletIcon' />\n                          Connect Wallet\n                        </Button>\n                      )}\n                    </Menu.Item>\n                  )}\n                </Menu.Items>\n              )}\n            </AnimatePresence>\n          </>\n        )}\n      </Menu>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/sidebar/sidebar-link.tsx",
    "content": "import { useRouter } from 'next/router';\nimport Link from 'next/link';\nimport cn from 'clsx';\nimport { preventBubbling } from '@lib/utils';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport type { NavLink } from './sidebar';\n\ntype SidebarLinkProps = NavLink & {\n  username?: string;\n};\n\nexport function SidebarLink({\n  href,\n  username,\n  iconName,\n  linkName,\n  disabled,\n  canBeHidden,\n  newTab\n}: SidebarLinkProps): JSX.Element {\n  const { asPath } = useRouter();\n  const isActive = username\n    ? asPath.includes(username)\n    : asPath\n        .split('/')\n        .slice(1, 2)\n        .includes(href.split('/')[1] || 'home');\n\n  return (\n    <Link\n      href={href}\n      className={cn(\n        'group py-1 outline-none',\n        canBeHidden ? 'hidden xs:flex' : 'flex',\n        disabled && 'cursor-not-allowed'\n      )}\n      onClick={disabled ? preventBubbling() : undefined}\n      target={newTab ? '_blank' : undefined}\n    >\n      <div\n        className={cn(\n          `custom-button flex items-center justify-center gap-4 self-start p-2 text-xl transition \n             duration-200 group-hover:bg-light-primary/10 group-focus-visible:ring-2 \n             group-focus-visible:ring-[#878a8c] dark:group-hover:bg-dark-primary/10 \n             dark:group-focus-visible:ring-white xs:p-3 xl:pr-5`,\n          isActive && 'font-bold'\n        )}\n      >\n        <HeroIcon\n          className={cn('h-7 w-7')}\n          iconName={iconName}\n          solid={isActive}\n        />\n        <p className='hidden xl:block'>{linkName}</p>\n      </div>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/sidebar/sidebar-profile.tsx",
    "content": "import { ActionModal } from '@components/modal/action-modal';\nimport { Modal } from '@components/modal/modal';\nimport { Button } from '@components/ui/button';\nimport { CustomIcon } from '@components/ui/custom-icon';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { UserAvatar } from '@components/user/user-avatar';\nimport { UserName } from '@components/user/user-name';\nimport { UserUsername } from '@components/user/user-username';\nimport { Menu } from '@headlessui/react';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport type { User } from '@lib/types/user';\nimport cn from 'clsx';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { SavePasskeyModal } from '../modal/save-passkey-modal';\nimport { variants } from './more-settings';\n\nexport function SidebarProfile(): JSX.Element {\n  const {\n    user,\n    usersWithKeys: users,\n    signOut,\n    setUser,\n    showAddAccountModal\n  } = useAuth();\n  const {\n    open: isLogoutModalOpen,\n    openModal: openLogoutModal,\n    closeModal: closeLogoutModal\n  } = useModal();\n  const {\n    open: isSavePasskeyModalOpen,\n    openModal: openSavePasskeyModal,\n    closeModal: closeSavePasskeyModal\n  } = useModal();\n\n  const { name, username, verified, photoURL } = user as User;\n\n  return (\n    <>\n      <Modal\n        modalClassName='max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={isLogoutModalOpen}\n        closeModal={closeLogoutModal}\n      >\n        <ActionModal\n          useIcon\n          focusOnMainBtn\n          title='Log out of Opencast?'\n          description='You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account.'\n          mainBtnLabel='Log out'\n          action={() => {\n            signOut();\n            closeLogoutModal();\n          }}\n          closeModal={closeLogoutModal}\n        />\n      </Modal>\n      <Modal\n        modalClassName='max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={isSavePasskeyModalOpen}\n        closeModal={closeSavePasskeyModal}\n      >\n        <SavePasskeyModal\n          closeSavePasskeyModal={closeSavePasskeyModal}\n          user={user}\n        ></SavePasskeyModal>\n      </Modal>\n      <Menu className='relative' as='section'>\n        {({ open }): JSX.Element => (\n          <>\n            <Menu.Button\n              className={cn(\n                `custom-button main-tab dark-bg-tab flex w-full items-center \n                 justify-between hover:bg-light-primary/10 active:bg-light-primary/20\n                 dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20`,\n                open && 'bg-light-primary/10 dark:bg-dark-primary/10'\n              )}\n            >\n              <div className='flex gap-3 truncate'>\n                <UserAvatar src={photoURL} alt={name} size={40} />\n                <div className='hidden truncate text-start leading-5 xl:block'>\n                  <UserName name={name} className='start' verified={verified} />\n                  <UserUsername username={username} disableLink />\n                </div>\n              </div>\n              <HeroIcon\n                className='hidden h-6 w-6 xl:block'\n                iconName='EllipsisHorizontalIcon'\n              />\n            </Menu.Button>\n            <AnimatePresence>\n              {open && (\n                <Menu.Items\n                  className='menu-container absolute bottom-20 left-0 right-0 w-60 xl:w-full'\n                  as={motion.div}\n                  {...variants}\n                  static\n                >\n                  {/* TODO: Mobile */}\n                  {users.map((menuUser) => {\n                    const { name, username, verified, photoURL, id } = menuUser;\n                    return (\n                      <Menu.Item\n                        className='flex items-center justify-between gap-4 border-b \n                               border-light-border hover:bg-light-primary/10 dark:border-dark-border'\n                        as='div'\n                      >\n                        {({ active }): JSX.Element => (\n                          <Button\n                            className={cn(\n                              'flex w-full items-center gap-3 rounded-md rounded-t-none',\n                              active && 'bg-main-sidebar-background'\n                            )}\n                            onClick={() => setUser(menuUser)}\n                          >\n                            <div className='flex flex-grow items-center gap-3 truncate'>\n                              <UserAvatar src={photoURL} alt={name} />\n                              <div className='truncate'>\n                                <UserName name={name} verified={verified} />\n                                <UserUsername username={username} disableLink />\n                              </div>\n                            </div>\n                            {user?.id === id && (\n                              <i>\n                                <HeroIcon\n                                  className='h-5 w-5 text-main-accent'\n                                  iconName='CheckIcon'\n                                />\n                              </i>\n                            )}\n                          </Button>\n                        )}\n                      </Menu.Item>\n                    );\n                  })}\n                  <Menu.Item>\n                    {({ active }): JSX.Element => (\n                      <Button\n                        className={cn(\n                          'flex w-full gap-3 rounded-md rounded-t-none p-4',\n                          active && 'bg-main-sidebar-background'\n                        )}\n                        onClick={showAddAccountModal}\n                      >\n                        <HeroIcon iconName='UserPlusIcon' />\n                        Add another account\n                      </Button>\n                    )}\n                  </Menu.Item>\n                  <Menu.Item>\n                    {({ active }): JSX.Element => (\n                      <Button\n                        className={cn(\n                          'flex w-full gap-3 rounded-md rounded-t-none p-4',\n                          active && 'bg-main-sidebar-background'\n                        )}\n                        onClick={openSavePasskeyModal}\n                      >\n                        <HeroIcon iconName='KeyIcon' />\n                        Save Signer Key\n                      </Button>\n                    )}\n                  </Menu.Item>\n                  <Menu.Item>\n                    {({ active }): JSX.Element => (\n                      <Button\n                        className={cn(\n                          'flex w-full gap-3 rounded-md rounded-t-none p-4',\n                          active && ' bg-main-sidebar-background'\n                        )}\n                        onClick={openLogoutModal}\n                      >\n                        <HeroIcon iconName='ArrowRightOnRectangleIcon' />\n                        <div className='truncate'>Log out @{username}</div>\n                      </Button>\n                    )}\n                  </Menu.Item>\n                  <i\n                    className='absolute -bottom-[10px] left-2 translate-x-1/2 rotate-180\n                               [filter:drop-shadow(#cfd9de_1px_-1px_1px)] \n                               dark:[filter:drop-shadow(#333639_1px_-1px_1px)]\n                               xl:left-1/2 xl:-translate-x-1/2'\n                  >\n                    <CustomIcon\n                      className='h-4 w-6 fill-main-background'\n                      iconName='TriangleIcon'\n                    />\n                  </i>\n                </Menu.Items>\n              )}\n            </AnimatePresence>\n          </>\n        )}\n      </Menu>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/sidebar/sidebar.tsx",
    "content": "import { Input } from '@components/input/input';\nimport { Modal } from '@components/modal/modal';\nimport { Button } from '@components/ui/button';\nimport { CustomIcon } from '@components/ui/custom-icon';\nimport type { IconName } from '@components/ui/hero-icon';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useWindow } from '@lib/context/window-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport Link from 'next/link';\nimport { MoreSettings } from './more-settings';\nimport { SidebarLink } from './sidebar-link';\nimport { SidebarProfile } from './sidebar-profile';\nimport { SyncView } from '@components/sync/sync-view';\n\nexport type NavLink = {\n  href: string;\n  linkName: string;\n  iconName: IconName;\n  disabled?: boolean;\n  canBeHidden?: boolean;\n  newTab?: boolean;\n};\n\nexport const navLinks: Readonly<NavLink[]> = [\n  {\n    href: '/home',\n    linkName: 'Home',\n    iconName: 'HomeIcon'\n  },\n  {\n    href: '/trends',\n    linkName: 'Topics',\n    iconName: 'ChatBubbleBottomCenterTextIcon'\n  }\n];\n\nexport function Sidebar(): JSX.Element {\n  const { user, userNotifications, resetNotifications } = useAuth();\n  const { isMobile } = useWindow();\n\n  const { open, openModal, closeModal } = useModal();\n\n  const username = user?.username as string;\n\n  return (\n    <header\n      id='sidebar'\n      className='flex w-0 shrink-0 transition-opacity duration-200 xs:w-20 md:w-24\n                 lg:max-w-none xl:-mr-4 xl:w-full xl:max-w-xs xl:justify-end'\n    >\n      <Modal\n        className='flex items-start justify-center'\n        modalClassName='bg-main-background rounded-2xl max-w-xl w-full mt-8 overflow-hidden'\n        open={open}\n        closeModal={closeModal}\n      >\n        <Input modal closeModal={closeModal} />\n      </Modal>\n      <div\n        className='fixed bottom-0 z-10 flex w-full flex-col justify-between border-t border-light-border \n                   bg-main-background py-0 dark:border-dark-border xs:top-0 xs:h-full xs:w-auto xs:border-0 \n                   xs:bg-transparent xs:px-2 xs:py-3 xs:pt-2 md:px-4 xl:w-72'\n      >\n        <section className='flex flex-col justify-center gap-2 xs:items-center xl:items-stretch'>\n          <h1 className='hidden xs:flex'>\n            <Link\n              href='/home'\n              className='custom-button main-tab text-accent-blue transition hover:bg-light-primary/10 \n                           focus-visible:bg-accent-blue/10 focus-visible:!ring-accent-blue/80\n                           dark:text-twitter-icon dark:hover:bg-dark-primary/10'\n            >\n              <CustomIcon className='h-7 w-7' iconName='TwitterIcon' />\n            </Link>\n          </h1>\n          <nav className='flex items-center justify-around xs:flex-col xs:justify-center xl:block'>\n            {navLinks.map(({ ...linkData }) => (\n              <SidebarLink {...linkData} key={linkData.href} />\n            ))}\n            {user?.keyPair && (\n              <>\n                <div\n                  onClick={() => {\n                    resetNotifications();\n                  }}\n                >\n                  {userNotifications && (\n                    <div className='absolute ml-6 mt-2 flex h-4 min-w-[16px] items-center rounded-full bg-main-accent text-white'>\n                      <div className='mx-auto px-1 text-xs'>\n                        {userNotifications < 100 ? userNotifications : '99+'}\n                      </div>\n                    </div>\n                  )}\n                  <SidebarLink\n                    // href='https://warpcast.com/~/notifications'\n                    href='/notifications'\n                    iconName='BellIcon'\n                    linkName={`Notifications`}\n                  />\n                </div>\n                <SidebarLink\n                  href={`/user/${username}`}\n                  username={username}\n                  linkName='Profile'\n                  iconName='UserIcon'\n                />\n                <SidebarLink\n                  href='/settings'\n                  linkName='Settings'\n                  iconName='Cog6ToothIcon'\n                />\n              </>\n            )}\n            {!isMobile && <MoreSettings />}\n          </nav>\n          {user?.keyPair && (\n            <div>\n              <Button\n                className='accent-tab absolute right-4 -translate-y-[72px] bg-main-accent text-lg font-bold text-white\n                       outline-none transition hover:brightness-90 active:brightness-75 xs:static xs:translate-y-0\n                       xs:hover:bg-main-accent/90 xs:active:bg-main-accent/75 xl:w-11/12'\n                onClick={openModal}\n              >\n                <CustomIcon\n                  className='block h-6 w-6 xl:hidden'\n                  iconName='FeatherIcon'\n                />\n                <p className='hidden xl:block'>Cast</p>\n              </Button>\n            </div>\n          )}\n        </section>\n        {!isMobile && user?.keyPair &&\n          <div className='gap-4 flex flex-col'>\n            <SyncView userId={user.id} />\n            <SidebarProfile />\n          </div>}\n        {!user?.keyPair && (\n          <Link\n            className='custom-button main-tab accent-tab absolute right-4 -translate-y-[72px] bg-main-accent text-center text-lg font-bold text-white\n                       outline-none transition hover:brightness-90 active:brightness-75 xs:static xs:translate-y-0\n                       xs:hover:bg-main-accent/90 xs:active:bg-main-accent/75 xl:w-11/12'\n            href='/login'\n          >\n            <CustomIcon\n              className='block h-6 w-6 xl:hidden'\n              iconName='FeatherIcon'\n            />\n            <p className='hidden xl:block'>Login</p>\n          </Link>\n        )}\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "src/components/sync/sync-view.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport useSWR from 'swr';\n\nexport function SyncView({ userId }: { userId?: string }) {\n  const [syncing, setSyncing] = useState(true);\n\n  const { data } = useSWR(\n    userId && syncing ? `/api/user/${userId}/sync` : undefined,\n    async (url) => {\n      const res = await fetch(url);\n      return res.json();\n    },\n    {\n      refreshInterval: 1000\n    }\n  );\n\n  useEffect(() => {\n    if (data && data.done) {\n      setSyncing(false);\n    }\n  }, [data]);\n\n  return (\n    <div>\n      {data && !data.done && (\n        <div className='flex flex-col gap-2'>\n          <div>Indexing...</div>\n          <div className='flex flex-col'>\n            <div title='Indexing' className='rounded-full border'>\n              <div\n                style={{\n                  width: `${Math.round(\n                    (data.completedCount / data.childCount) * 100\n                  )}%`\n                }}\n                className={`flex min-w-[20%] items-center rounded-full bg-light-primary px-2 text-right dark:bg-dark-primary`}\n              >\n                <div className='ml-auto text-white dark:text-black'>\n                  {Math.round((data.completedCount / data.childCount) * 100)}%\n                </div>\n              </div>\n            </div>\n            <div className='text-light-secondary dark:text-dark-secondary'>\n              {data.status}\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/number-stats.tsx",
    "content": "import { AnimatePresence, motion } from 'framer-motion';\nimport { getStatsMove } from '@lib/utils';\nimport { formatNumber } from '@lib/date';\n\ntype NumberStatsProps = {\n  move: number;\n  stats: number;\n  alwaysShowStats?: boolean;\n};\n\nexport function NumberStats({\n  move,\n  stats,\n  alwaysShowStats\n}: NumberStatsProps): JSX.Element {\n  return (\n    <div className='overflow-hidden'>\n      <AnimatePresence mode='wait' initial={false}>\n        {(alwaysShowStats || !!stats) && (\n          <motion.p className='text-sm' {...getStatsMove(move)} key={stats}>\n            {formatNumber(stats)}\n          </motion.p>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/stats-empty.tsx",
    "content": "import cn from 'clsx';\nimport { NextImage } from '@components/ui/next-image';\nimport type { ImageData } from '@lib/types/file';\n\nexport type StatsEmptyProps = {\n  title: string;\n  modal?: boolean;\n  imageData?: ImageData;\n  description: string;\n};\n\nexport function StatsEmpty({\n  title,\n  modal,\n  imageData,\n  description\n}: StatsEmptyProps): JSX.Element {\n  return (\n    <div className={cn('flex justify-center p-8', modal && 'mt-[52px]')}>\n      <div className='w-full max-w-sm'>\n        <div className='flex flex-col items-center gap-6'>\n          {imageData && (\n            <NextImage\n              width={336}\n              height={168}\n              src={imageData.src}\n              alt={imageData.alt}\n            />\n          )}\n          <div className='flex flex-col gap-2 text-center'>\n            <p className='text-3xl font-extrabold'>{title}</p>\n            <p className='text-light-secondary dark:text-dark-secondary'>\n              {description}\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-actions.tsx",
    "content": "import { ActionModal } from '@components/modal/action-modal';\nimport { Modal } from '@components/modal/modal';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { CastAddBody, Message } from '@farcaster/hub-web';\nimport { Dialog, Popover } from '@headlessui/react';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport type { Tweet } from '@lib/types/tweet';\nimport type { User, UserFullResponse, UserResponse } from '@lib/types/user';\nimport { preventBubbling } from '@lib/utils';\nimport cn from 'clsx';\nimport type { Variants } from 'framer-motion';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport Link from 'next/link';\nimport { useEffect, useState } from 'react';\nimport { toast } from 'react-hot-toast';\nimport useSWR from 'swr';\nimport { useChainId } from 'wagmi';\nimport {\n  createCastMessage,\n  createFollowMessage,\n  createRemoveCastMessage,\n  submitHubMessage\n} from '../../lib/farcaster/utils';\nimport { fetchJSON } from '../../lib/fetch';\nimport { BaseResponse } from '../../lib/types/responses';\nimport { TopicResponse, TopicType } from '../../lib/types/topic';\nimport { TipModal } from '../modal/tip-modal';\nimport { SearchTopics } from '../search/search-topics';\nimport { Loading } from '../ui/loading';\nimport { TopicView } from './tweet-topic';\n\nexport const variants: Variants = {\n  initial: { opacity: 0, y: -25 },\n  animate: {\n    opacity: 1,\n    y: 0,\n    transition: { type: 'spring', duration: 0.4 }\n  },\n  exit: { opacity: 0, y: -25, transition: { duration: 0.2 } }\n};\n\ntype TweetActionsProps = Pick<Tweet, 'createdBy'> & {\n  isOwner: boolean;\n  ownerId: string;\n  tweetId: string;\n  username: string;\n  parentId?: string;\n  hasImages: boolean;\n  viewTweet?: boolean;\n  topic?: TopicType;\n};\n\nexport function TweetActions({\n  isOwner,\n  tweetId,\n  username,\n  createdBy,\n  topic\n}: TweetActionsProps): JSX.Element {\n  const { user: currentUser, isAdmin } = useAuth();\n\n  const chainId = useChainId();\n\n  const [shouldFetchUser, setShouldFetchUser] = useState(false);\n  const { data: user, isValidating: isUserLoading } = useSWR(\n    shouldFetchUser ? `/api/user/${createdBy}` : null,\n    async (url) => (await fetchJSON<UserFullResponse>(url)).result,\n    { revalidateOnFocus: false }\n  );\n\n  const {\n    open: removeOpen,\n    openModal: removeOpenModal,\n    closeModal: removeCloseModal\n  } = useModal();\n\n  const {\n    open: repostOpen,\n    openModal: repostOpenModal,\n    closeModal: repostCloseModal\n  } = useModal();\n\n  const {\n    open: tipUserOpen,\n    openModal: tipOpenModal,\n    closeModal: tipCloseModal\n  } = useModal();\n\n  const [repostTopicUrl, setRepostTopicUrl] = useState<string>();\n  const [showingTopicSelector, setShowingTopicSelector] = useState(false);\n  const [repostTopic, setRepostTopic] = useState<TopicType | null>(\n    topic || null\n  );\n  const [repostModalLoading, setRepostModalLoading] = useState(false);\n\n  const { data: topicResult, isValidating: loadingTopic } = useSWR(\n    repostTopicUrl\n      ? `/api/topic?url=${encodeURIComponent(repostTopicUrl)}`\n      : null,\n    async (url) => {\n      const res = await fetchJSON<TopicResponse>(url);\n      return res.result;\n    },\n    { revalidateOnFocus: false }\n  );\n\n  const { id: userId } = currentUser as User;\n  const isInAdminControl = isAdmin && !isOwner;\n\n  const handleRemove = async (): Promise<void> => {\n    const message = await createRemoveCastMessage({\n      castHash: tweetId,\n      castAuthorFid: parseInt(userId)\n    });\n\n    if (message) {\n      const res = await submitHubMessage(message);\n\n      toast.success(\n        `${isInAdminControl ? `@${username}'s` : 'Your'} Cast was deleted`\n      );\n\n      removeCloseModal();\n    } else {\n      toast.error(`Failed to delete cast`);\n    }\n  };\n\n  const handleFollow =\n    (closeMenu: () => void, type: 'follow' | 'unfollow') =>\n    async (): Promise<void> => {\n      const message = await createFollowMessage({\n        fid: parseInt(createdBy),\n        targetFid: parseInt(userId),\n        remove: type === 'unfollow'\n      });\n\n      if (message) {\n        const res = await submitHubMessage(message);\n\n        closeMenu();\n\n        toast.success(\n          `You ${type === 'follow' ? 'followed' : 'unfollowed'} @${username}`\n        );\n      } else {\n        toast.error(\n          `Failed to ${type === 'follow' ? 'follow' : 'unfollow'} @${username}`\n        );\n      }\n    };\n\n  const handleRepostInChannel = async (): Promise<void> => {\n    // Set loading\n    setRepostModalLoading(true);\n    // Get original message\n    const messageJson = (\n      await fetchJSON<BaseResponse<any>>(\n        `/api/hub?hash=${tweetId}&fid=${createdBy}`\n      )\n    ).result;\n    const message = Message.fromJSON(messageJson);\n    if (!message) {\n      toast.error('Failed to get original message');\n      setRepostModalLoading(false);\n      return;\n    }\n    const castAddBody = CastAddBody.fromJSON(message.data?.castAddBody);\n    if (!castAddBody) {\n      toast.error('Failed to get original message');\n      setRepostModalLoading(false);\n      return;\n    }\n\n    // Replace topic with new topic\n    const newCastAddBody = await createCastMessage({\n      text: castAddBody.text,\n      embeds: castAddBody.embeds,\n      parentCastHash: castAddBody.parentCastId?.hash\n        ? Buffer.from(castAddBody.parentCastId?.hash).toString('hex')\n        : undefined,\n      parentCastFid: castAddBody.parentCastId?.fid,\n      parentUrl: repostTopic?.url,\n      mentions: castAddBody.mentions,\n      mentionsPositions: castAddBody.mentionsPositions,\n      fid: parseInt(createdBy)\n    });\n    if (!newCastAddBody) {\n      toast.error('Failed to create new message');\n      setRepostModalLoading(false);\n      return;\n    }\n\n    // Submit new message\n    const res = await submitHubMessage(newCastAddBody);\n    if (!res) {\n      toast.error('Failed to repost message');\n      setRepostModalLoading(false);\n      return;\n    }\n\n    const newMessage = Message.fromJSON(res);\n    const newCastId = Buffer.from(newMessage.hash).toString('hex');\n\n    setRepostModalLoading(false);\n    repostCloseModal();\n\n    toast.success(\n      () => (\n        <span className='flex gap-2'>\n          Your cast was reposted\n          <Link\n            href={`/tweet/${newCastId}`}\n            className='custom-underline font-bold'\n          >\n            View\n          </Link>\n        </span>\n      ),\n      { duration: 6000 }\n    );\n  };\n\n  // const userIsFollowed = following.includes(createdBy);\n\n  const handleOpenInWarpcast = (closeMenu: () => void) => {\n    closeMenu();\n    window.open(\n      `https://warpcast.com/${username}/0x${tweetId.slice(0, 5)}`,\n      '_blank'\n    );\n  };\n\n  useEffect(() => {\n    if (repostTopicUrl === repostTopic?.url || repostTopic === undefined)\n      return;\n    setRepostTopicUrl(repostTopic?.url);\n  }, [repostTopic]);\n\n  useEffect(() => {\n    if (topicResult && repostTopic?.url !== topicResult.url) {\n      setRepostTopic(topicResult);\n    }\n  }, [topicResult]);\n\n  useEffect(() => {\n    if (!tipUserOpen) return;\n    setShouldFetchUser(true);\n  }, [tipUserOpen]);\n\n  return (\n    <>\n      <Modal\n        modalClassName='max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={removeOpen}\n        closeModal={removeCloseModal}\n      >\n        <ActionModal\n          title='Delete Cast?'\n          description={`This can’t be undone and it will be removed from ${\n            isInAdminControl ? `@${username}'s` : 'your'\n          } profile, the timeline of any accounts that follow ${\n            isInAdminControl ? `@${username}` : 'you'\n          }, and from Farcaster search results.`}\n          mainBtnClassName='bg-accent-red hover:bg-accent-red/90 active:bg-accent-red/75 accent-tab\n                            focus-visible:bg-accent-red/90'\n          mainBtnLabel='Delete'\n          focusOnMainBtn\n          action={handleRemove}\n          closeModal={removeCloseModal}\n        />\n      </Modal>\n      <Modal\n        modalClassName='max-w-sm bg-main-background w-full p-8 rounded-2xl'\n        open={repostOpen}\n        closeModal={repostCloseModal}\n      >\n        <div className='flex flex-col gap-6'>\n          <div className='flex flex-col gap-4'>\n            <div className='flex flex-col gap-2'>\n              <div className='flex items-center'>\n                <i className='inline pr-2'>\n                  <HeroIcon iconName='ArrowPathRoundedSquareIcon' />\n                </i>\n                <Dialog.Title className='inline text-xl font-bold'>\n                  Repost to topic\n                </Dialog.Title>\n              </div>\n              <Dialog.Description className='text-light-secondary dark:text-dark-secondary'>\n                Repost this Cast to another topic\n              </Dialog.Description>\n            </div>\n          </div>\n          {loadingTopic ? (\n            <div className='w-10'>\n              <Loading />\n            </div>\n          ) : showingTopicSelector ? (\n            <SearchTopics\n              enabled={repostOpen}\n              onSelectRawUrl={(url) => setRepostTopicUrl(url)}\n              onSelectTopic={(topic) => setRepostTopic(topic)}\n              setShowing={setShowingTopicSelector}\n            />\n          ) : (\n            repostTopic && (\n              <div\n                className='cursor-pointer text-light-secondary dark:text-dark-secondary'\n                onClick={() => setShowingTopicSelector(true)}\n              >\n                <span className='inline'>\n                  <TopicView topic={repostTopic} />\n                </span>\n              </div>\n            )\n          )}\n          {repostModalLoading ? (\n            <Loading className='w-full' />\n          ) : (\n            <Button\n              className='accent-tab flex items-center justify-center bg-main-accent font-bold text-white enabled:hover:bg-main-accent/90 enabled:active:bg-main-accent/75'\n              onClick={() => {\n                handleRepostInChannel();\n              }}\n              disabled={repostModalLoading}\n            >\n              Repost\n            </Button>\n          )}\n        </div>\n      </Modal>\n      <TipModal\n        isUserLoading={isUserLoading}\n        tipCloseModal={tipCloseModal}\n        tipUserOpen={tipUserOpen}\n        user={user}\n        username={username}\n      />\n      <Popover>\n        {({ open, close }): JSX.Element => (\n          <>\n            <Popover.Button\n              as={Button}\n              className={cn(\n                `main-tab group group absolute right-2 top-2 p-2 \n                 hover:bg-accent-blue/10 focus-visible:bg-accent-blue/10\n                 focus-visible:!ring-accent-blue/80 active:bg-accent-blue/20`,\n                open && 'bg-accent-blue/10 [&>div>svg]:text-accent-blue'\n              )}\n            >\n              <div className='group relative'>\n                <HeroIcon\n                  className='h-5 w-5 text-light-secondary group-hover:text-accent-blue\n                             group-focus-visible:text-accent-blue dark:text-dark-secondary/80'\n                  iconName='EllipsisHorizontalIcon'\n                />\n                {!open && <ToolTip tip='More' />}\n              </div>\n            </Popover.Button>\n            <AnimatePresence>\n              {open && (\n                <Popover.Panel\n                  className='menu-container group absolute right-2 top-[50px] whitespace-nowrap text-light-primary \n                             dark:text-dark-primary'\n                  as={motion.div}\n                  {...variants}\n                  static\n                >\n                  {(isAdmin || isOwner) && (\n                    <Popover.Button\n                      className='accent-tab flex w-full gap-3 rounded-md rounded-b-none p-4 text-accent-red\n                                 hover:bg-main-sidebar-background'\n                      as={Button}\n                      onClick={preventBubbling(removeOpenModal)}\n                    >\n                      <HeroIcon iconName='TrashIcon' />\n                      Delete\n                    </Popover.Button>\n                  )}\n\n                  {/* {userIsFollowed ? (\n                    <Popover.Button\n                      className='accent-tab flex w-full gap-3 rounded-md rounded-t-none p-4 hover:bg-main-sidebar-background'\n                      as={Button}\n                      onClick={preventBubbling(handleFollow(close, 'unfollow'))}\n                    >\n                      <HeroIcon iconName='UserMinusIcon' />\n                      Unfollow @{username}\n                    </Popover.Button>\n                  ) : (\n                    <Popover.Button\n                      className='accent-tab flex w-full gap-3 rounded-md rounded-t-none p-4 hover:bg-main-sidebar-background'\n                      as={Button}\n                      onClick={preventBubbling(handleFollow(close, 'follow'))}\n                    >\n                      <HeroIcon iconName='UserPlusIcon' />\n                      Follow @{username}\n                    </Popover.Button>\n                  )} */}\n                  {isOwner && (\n                    <Popover.Button\n                      className='accent-tab flex w-full gap-3 rounded-md rounded-t-none p-4 hover:bg-main-sidebar-background'\n                      as={Button}\n                      onClick={preventBubbling(async (): Promise<void> => {\n                        close();\n                        setShowingTopicSelector(true);\n                        repostOpenModal();\n                      })}\n                    >\n                      <HeroIcon iconName='ArrowPathRoundedSquareIcon' />\n                      Repost to topic\n                    </Popover.Button>\n                  )}\n                  {\n                    <Popover.Button\n                      className='accent-tab flex w-full gap-3 rounded-md rounded-t-none p-4 hover:bg-main-sidebar-background'\n                      as={Button}\n                      onClick={preventBubbling(() => {\n                        close();\n                        tipOpenModal();\n                      })}\n                    >\n                      <HeroIcon iconName='BanknotesIcon' />\n                      Tip user\n                    </Popover.Button>\n                  }\n\n                  <Popover.Button\n                    className='accent-tab flex w-full gap-3 rounded-md rounded-t-none p-4 hover:bg-main-sidebar-background'\n                    as={Button}\n                    onClick={preventBubbling(() => handleOpenInWarpcast(close))}\n                  >\n                    <HeroIcon iconName='ArrowTopRightOnSquareIcon' />\n                    Open in Warpcast\n                  </Popover.Button>\n                </Popover.Panel>\n              )}\n            </AnimatePresence>\n          </>\n        )}\n      </Popover>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-date.tsx",
    "content": "import Link from 'next/link';\nimport cn from 'clsx';\nimport { formatDate } from '@lib/date';\nimport { ToolTip } from '@components/ui/tooltip';\nimport type { Tweet } from '@lib/types/tweet';\n\ntype TweetDateProps = Pick<Tweet, 'createdAt'> & {\n  tweetLink: string;\n  viewTweet?: boolean;\n};\n\nexport function TweetDate({\n  createdAt,\n  tweetLink,\n  viewTweet\n}: TweetDateProps): JSX.Element {\n  return (\n    <div className={cn('flex gap-1')}>\n      {!viewTweet && <i>·</i>}\n      <div className='group relative'>\n        <Link\n          href={tweetLink}\n          className={cn(\n            'custom-underline peer whitespace-nowrap',\n            viewTweet && 'text-light-secondary dark:text-dark-secondary'\n          )}\n        >\n          {formatDate(new Date(createdAt), viewTweet ? 'full' : 'tweet')}\n        </Link>\n        <ToolTip\n          className='translate-y-1 peer-focus:opacity-100 peer-focus-visible:visible\n                     peer-focus-visible:delay-200'\n          tip={formatDate(new Date(createdAt), 'full')}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-embed.tsx",
    "content": "import Link from 'next/link';\r\nimport { useMemo, useState } from 'react';\r\nimport useSWR from 'swr';\r\nimport { ExternalEmbed } from '../../lib/types/tweet';\r\nimport { NextImage } from '../ui/next-image';\r\nimport { ImagePreview } from '@components/input/image-preview';\r\nimport { Frame } from '@components/frames/Frame';\r\nimport { Frame as FrameType } from 'frames.js';\r\n\r\nconst hoverModifier =\r\n  'hover:brightness-75 dark:hover:brightness-125 hover:duration-200 transition';\r\n\r\nexport function TweetEmbeds({\r\n  embeds,\r\n  tweetId,\r\n  tweetAuthorId\r\n}: {\r\n  embeds: ExternalEmbed[];\r\n  tweetId: string;\r\n  tweetAuthorId: string;\r\n}) {\r\n  const fetchEmbeds = async (url: string | null) => {\r\n    if (!url) return null;\r\n\r\n    const res = await fetch(url);\r\n    if (!res.ok) {\r\n      return null;\r\n    }\r\n\r\n    const data = (await res.json()) as (ExternalEmbed | null)[];\r\n    return data;\r\n  };\r\n\r\n  const url = useMemo(() => {\r\n    return embeds.map((embed) => embed.url).join(',');\r\n  }, embeds);\r\n\r\n  const { data: embedsData } = useSWR(`/api/embeds?urls=${url}`, fetchEmbeds, {\r\n    revalidateOnFocus: false\r\n  });\r\n\r\n  const embedsCount = useMemo(() => {\r\n    return embedsData?.filter((embed) => embed !== null).length || 0;\r\n  }, [embedsData]);\r\n\r\n  const imageEmbeds = useMemo(() => {\r\n    return embedsData?.filter((embed) =>\r\n      embed?.contentType?.startsWith('image/')\r\n    );\r\n  }, [embedsData]);\r\n\r\n  const frames = useMemo(() => {\r\n    return embedsData?.filter((embed) => embed?.frame);\r\n  }, [embedsData]);\r\n\r\n  return embedsData !== undefined ? (\r\n    embedsData && embedsCount > 0 && (\r\n      <div>\r\n        {imageEmbeds && imageEmbeds.length > 0 && (\r\n          <div>\r\n            <ImagePreview\r\n              tweet\r\n              imagesPreview={imageEmbeds.map((e) => ({\r\n                alt: e?.title || '',\r\n                id: e!.url,\r\n                src: e!.url\r\n              }))}\r\n              previewCount={imageEmbeds.length}\r\n            />\r\n          </div>\r\n        )}\r\n        <div className={embedsCount > 1 ? `mt-2 grid gap-2` : 'mt-2'}>\r\n          {embedsData?.map((embed, index) =>\r\n            embed &&\r\n            !embed.contentType?.startsWith('image/') &&\r\n            !embed.frame ? (\r\n              <TweetEmbed {...embed} key={index}></TweetEmbed>\r\n            ) : (\r\n              <></>\r\n            )\r\n          )}\r\n        </div>\r\n        {frames?.map((embed) => (\r\n          <div key={embed?.url}>\r\n            <FramePreview\r\n              embed={embed!}\r\n              url={embed!.url}\r\n              tweetAuthorId={tweetAuthorId}\r\n              tweetId={tweetId}\r\n            />\r\n          </div>\r\n        ))}\r\n      </div>\r\n    )\r\n  ) : (\r\n    <div className={embeds.length > 1 ? `mt-2 grid gap-2` : 'mt-2'}>\r\n      {embeds?.map((embed, index) =>\r\n        embed ? (\r\n          <TweetEmbed {...embed} key={index} isLoading={true}></TweetEmbed>\r\n        ) : (\r\n          <></>\r\n        )\r\n      )}\r\n    </div>\r\n  );\r\n}\r\n\r\nexport function TweetEmbed({\r\n  title,\r\n  text,\r\n  image,\r\n  provider,\r\n  url,\r\n  icon,\r\n  isLoading,\r\n  newTab,\r\n  buttonTitle,\r\n  onButtonClick\r\n}: ExternalEmbed & {\r\n  isLoading?: boolean;\r\n  newTab?: boolean;\r\n  buttonTitle?: string;\r\n  onButtonClick?: () => void;\r\n}): JSX.Element {\r\n  const imageEl = (\r\n    <div>\r\n      <div className='ml-auto h-[100px] w-[100px] overflow-hidden rounded-md'>\r\n        {image ? (\r\n          <img\r\n            src={image}\r\n            alt={title || ''}\r\n            title={title || 'Unknown'}\r\n            className='h-full w-full object-cover'\r\n          />\r\n        ) : isLoading ? (\r\n          <div className='animate-pulse rounded-md bg-light-secondary dark:bg-dark-secondary sm:block'></div>\r\n        ) : (\r\n          <div className='rounded-md bg-light-secondary dark:bg-dark-secondary sm:block'></div>\r\n        )}\r\n      </div>\r\n    </div>\r\n  );\r\n\r\n  const link = (\r\n    <div>\r\n      <Link\r\n        href={url}\r\n        className='override-nav'\r\n        target={newTab ? '_blank' : url.startsWith('/') ? undefined : '_blank'}\r\n      >\r\n        <div\r\n          className='flex w-full gap-2 rounded-md border \r\nborder-black border-light-border p-2 text-left text-sm dark:border-dark-border'\r\n        >\r\n          {buttonTitle ? imageEl : <></>}\r\n          <div className='flex min-w-0 flex-grow flex-col justify-center'>\r\n            <div className='break-word flex items-center [overflow-wrap:anywhere]'>\r\n              {icon && (\r\n                // Only fully rounded if it's a link to a cast\r\n                <span\r\n                  className={`mx-1 flex-shrink-0 overflow-hidden ${\r\n                    url.startsWith('/tweet') ? 'rounded-full' : 'rounded-sm'\r\n                  }`}\r\n                >\r\n                  <img\r\n                    src={icon}\r\n                    alt={provider || ''}\r\n                    className='h-4 w-4 object-cover'\r\n                  />\r\n                </span>\r\n              )}\r\n              {title && (\r\n                <span\r\n                  className={`mx-1 line-clamp-2 text-ellipsis ${hoverModifier}`}\r\n                >\r\n                  {title}\r\n                </span>\r\n              )}\r\n            </div>\r\n            {text ? (\r\n              <span\r\n                className={`mx-1 line-clamp-3 text-gray-400 ${hoverModifier}`}\r\n              >\r\n                {text}\r\n              </span>\r\n            ) : isLoading ? (\r\n              <div className='h-12 w-full animate-pulse rounded-md bg-light-secondary dark:bg-dark-secondary'></div>\r\n            ) : (\r\n              <></>\r\n            )}\r\n          </div>\r\n          {!buttonTitle ? (\r\n            imageEl\r\n          ) : (\r\n            <div className='my-auto flex'>\r\n              <button\r\n                onClick={onButtonClick}\r\n                className='rounded-md bg-main-accent p-2 font-bold'\r\n              >\r\n                {buttonTitle}\r\n              </button>\r\n            </div>\r\n          )}\r\n        </div>\r\n      </Link>\r\n      <div className='text-right text-sm text-light-secondary dark:text-dark-secondary'>\r\n        {URL.canParse(url) ? new URL(url).hostname : ''}\r\n      </div>\r\n    </div>\r\n  );\r\n\r\n  return link;\r\n}\r\n\r\nexport function FramePreview({\r\n  url,\r\n  tweetAuthorId,\r\n  tweetId,\r\n  embed: { frame, ...embed }\r\n}: {\r\n  url: string;\r\n  tweetAuthorId: string;\r\n  tweetId: string;\r\n  embed: ExternalEmbed;\r\n}) {\r\n  const [showFrame, setShowFrame] = useState(false);\r\n\r\n  const handleToggleFrame = () => {\r\n    setShowFrame(!showFrame);\r\n  };\r\n\r\n  return showFrame && frame ? (\r\n    <div className='override-nav flex justify-center rounded-md'>\r\n      <Frame\r\n        url={url}\r\n        frame={frame}\r\n        frameContext={{\r\n          castId: {\r\n            fid: parseInt(tweetAuthorId),\r\n            hash: `0x${tweetId}`\r\n          }\r\n        }}\r\n      />\r\n    </div>\r\n  ) : (\r\n    <div\r\n      onClick={(e) => {\r\n        e.preventDefault();\r\n        e.stopPropagation();\r\n        handleToggleFrame();\r\n      }}\r\n    >\r\n      <TweetEmbed\r\n        {...embed}\r\n        buttonTitle={'View'}\r\n        onButtonClick={handleToggleFrame}\r\n        url={url}\r\n      ></TweetEmbed>\r\n    </div>\r\n  );\r\n}\r\n"
  },
  {
    "path": "src/components/tweet/tweet-option.tsx",
    "content": "import cn from 'clsx';\nimport { preventBubbling } from '@lib/utils';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { NumberStats } from './number-stats';\nimport type { IconName } from '@components/ui/hero-icon';\n\ntype TweetOption = {\n  tip: string;\n  move?: number;\n  stats?: number;\n  iconName: IconName;\n  disabled?: boolean;\n  className: string;\n  viewTweet?: boolean;\n  iconClassName: string;\n  onClick?: (...args: unknown[]) => unknown;\n};\n\nexport function TweetOption({\n  tip,\n  move,\n  stats,\n  disabled,\n  iconName,\n  className,\n  viewTweet,\n  iconClassName,\n  onClick\n}: TweetOption): JSX.Element {\n  return (\n    <button\n      className={cn(\n        `group flex items-center gap-1.5 p-0 transition-none\n         disabled:cursor-not-allowed inner:transition inner:duration-200`,\n        disabled && 'cursor-not-allowed',\n        className\n      )}\n      onClick={preventBubbling(onClick)}\n    >\n      <i\n        className={cn(\n          'relative rounded-full p-2 not-italic group-focus-visible:ring-2',\n          iconClassName\n        )}\n      >\n        <HeroIcon\n          className={viewTweet ? 'h-6 w-6' : 'h-5 w-5'}\n          iconName={iconName}\n        />\n        <ToolTip tip={tip} />\n      </i>\n      {!viewTweet && (\n        <NumberStats move={move as number} stats={stats as number} />\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-parent.tsx",
    "content": "import { useMemo, useEffect } from 'react';\nimport { doc } from 'firebase/firestore';\nimport { getRandomId } from '@lib/random';\nimport { Tweet } from './tweet';\nimport type { LoadedParents } from './tweet-with-parent';\nimport useSWR from 'swr';\nimport { fetchJSON } from '../../lib/fetch';\nimport { TweetResponse, populateTweetUsers } from '../../lib/types/tweet';\n\ntype TweetParentProps = {\n  parentId: string;\n  loadedParents: LoadedParents;\n  addParentId: (parentId: string, componentId: string) => void;\n};\n\nexport function TweetParent({\n  parentId,\n  loadedParents,\n  addParentId\n}: TweetParentProps): JSX.Element | null {\n  const componentId = useMemo(getRandomId, []);\n\n  const isParentAlreadyLoaded = loadedParents.some(\n    (child) => child.childId === componentId\n  );\n\n  const { data, isValidating: loading } = useSWR(\n    `/api/tweet/${parentId}`,\n    async (url) => (await fetchJSON<TweetResponse>(url)).result,\n    { revalidateOnFocus: false, revalidateOnReconnect: false }\n  );\n\n  useEffect(() => {\n    addParentId(parentId, componentId);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const skeletonClass = `animate-pulse bg-light-secondary dark:bg-dark-secondary`;\n\n  if (loading || !data)\n    return (\n      <div className={`flex h-32 gap-x-3 px-4 pt-3`}>\n        <div className='flex flex-col items-center'>\n          <div\n            className={`mb-2 h-12 w-12 flex-shrink-0 flex-grow-0 rounded-full ${skeletonClass}`}\n          ></div>\n          <i className='hover-animation h-full w-0.5 bg-light-line-reply dark:bg-dark-line-reply' />\n        </div>\n        <div className={`h-20 w-full rounded-md ${skeletonClass}`}></div>\n      </div>\n    );\n\n  return (\n    <Tweet\n      parentTweet\n      {...populateTweetUsers(data, data.users)}\n      user={data.users[data.createdBy]}\n    />\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-parent.tsx.bak",
    "content": "import { useMemo, useEffect } from 'react';\nimport { doc } from 'firebase/firestore';\nimport { useDocument } from '@lib/hooks/useDocument';\nimport { tweetsCollection } from '@lib/firebase/collections';\nimport { getRandomId } from '@lib/random';\nimport { Tweet } from './tweet';\nimport type { LoadedParents } from './tweet-with-parent.tsx.bak';\n\ntype TweetParentProps = {\n  parentId: string;\n  loadedParents: LoadedParents;\n  addParentId: (parentId: string, componentId: string) => void;\n};\n\nexport function TweetParent({\n  parentId,\n  loadedParents,\n  addParentId\n}: TweetParentProps): JSX.Element | null {\n  const componentId = useMemo(getRandomId, []);\n\n  const isParentAlreadyLoaded = loadedParents.some(\n    (child) => child.childId === componentId\n  );\n\n  const { data, loading } = useDocument(doc(tweetsCollection, parentId), {\n    includeUser: true,\n    allowNull: true,\n    disabled: isParentAlreadyLoaded\n  });\n\n  useEffect(() => {\n    addParentId(parentId, componentId);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  if (loading || !isParentAlreadyLoaded || !data) return null;\n\n  return <Tweet parentTweet {...data} />;\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-share.tsx",
    "content": "import Link from 'next/link';\nimport cn from 'clsx';\nimport { Popover } from '@headlessui/react';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { toast } from 'react-hot-toast';\nimport { useAuth } from '@lib/context/auth-context';\n// import { manageBookmark } from '@lib/firebase/utils';\nimport { preventBubbling } from '@lib/utils';\nimport { siteURL } from '@lib/env';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { variants } from './tweet-actions';\n\ntype TweetShareProps = {\n  userId: string;\n  tweetId: string;\n  viewTweet?: boolean;\n};\n\nexport function TweetShare({\n  userId,\n  tweetId,\n  viewTweet\n}: TweetShareProps): JSX.Element {\n  const { userBookmarks } = useAuth();\n\n  // const handleBookmark =\n  //   (closeMenu: () => void, ...args: Parameters<typeof manageBookmark>) =>\n  //   async (): Promise<void> => {\n  //     const [type] = args;\n\n  //     closeMenu();\n  //     await manageBookmark(...args);\n\n  //     toast.success(\n  //       type === 'bookmark'\n  //         ? (): JSX.Element => (\n  //             <span className='flex gap-2'>\n  //               Tweet added to your Bookmarks\n  //               <Link href='/bookmarks'>\n  //                 <a className='custom-underline font-bold'>View</a>\n  //               </Link>\n  //             </span>\n  //           )\n  //         : 'Tweet removed from your bookmarks'\n  //     );\n  //   };\n\n  const handleCopy = (closeMenu: () => void) => async (): Promise<void> => {\n    closeMenu();\n    await navigator.clipboard.writeText(`${siteURL}/tweet/${tweetId}`);\n    toast.success('Copied to clipboard');\n  };\n\n  const tweetIsBookmarked = !!userBookmarks?.some(({ id }) => id === tweetId);\n\n  return (\n    <Popover className='relative'>\n      {({ open, close }): JSX.Element => (\n        <>\n          <Popover.Button\n            className={cn(\n              `group relative flex items-center gap-1 p-0 outline-none \n               transition-none hover:text-accent-blue focus-visible:text-accent-blue`,\n              open && 'text-accent-blue inner:bg-accent-blue/10'\n            )}\n          >\n            <i\n              className='relative rounded-full p-2 not-italic duration-200 group-hover:bg-accent-blue/10 \n                         group-focus-visible:bg-accent-blue/10 group-focus-visible:ring-2 \n                         group-focus-visible:ring-accent-blue/80 group-active:bg-accent-blue/20'\n            >\n              <HeroIcon\n                className={viewTweet ? 'h-6 w-6' : 'h-5 w-5'}\n                iconName='ArrowUpTrayIcon'\n              />\n              {!open && <ToolTip tip='Share' />}\n            </i>\n          </Popover.Button>\n          <AnimatePresence>\n            {open && (\n              <Popover.Panel\n                className='menu-container group absolute right-0 top-11 whitespace-nowrap text-light-primary dark:text-dark-primary'\n                as={motion.div}\n                {...variants}\n                static\n              >\n                <Popover.Button\n                  className='accent-tab flex w-full gap-3 rounded-md rounded-b-none p-4 hover:bg-main-sidebar-background'\n                  as={Button}\n                  onClick={preventBubbling(handleCopy(close))}\n                >\n                  <HeroIcon iconName='LinkIcon' />\n                  Copy link to Cast\n                </Popover.Button>\n                {/* {!tweetIsBookmarked ? (\n                  <Popover.Button\n                    className='accent-tab flex w-full gap-3 rounded-md rounded-t-none p-4 hover:bg-main-sidebar-background'\n                    as={Button}\n                    // onClick={preventBubbling(\n                    //   handleBookmark(close, 'bookmark', userId, tweetId)\n                    // )}\n                  >\n                    <HeroIcon iconName='BookmarkIcon' />\n                    Bookmark\n                  </Popover.Button>\n                ) : (\n                  <Popover.Button\n                    className='accent-tab flex w-full gap-3 rounded-md rounded-t-none p-4 hover:bg-main-sidebar-background'\n                    as={Button}\n                    // onClick={preventBubbling(\n                    //   handleBookmark(close, 'unbookmark', userId, tweetId)\n                    // )}\n                  >\n                    <HeroIcon iconName='BookmarkSlashIcon' />\n                    Remove Tweet from Bookmarks\n                  </Popover.Button>\n                )} */}\n              </Popover.Panel>\n            )}\n          </AnimatePresence>\n        </>\n      )}\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-stats.tsx",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport cn from 'clsx';\nimport { useEffect, useMemo, useState } from 'react';\nimport { ViewTweetStats } from '@components/view/view-tweet-stats';\nimport type { Tweet } from '@lib/types/tweet';\nimport {\n  createReactionMessage,\n  submitHubMessage\n} from '../../lib/farcaster/utils';\nimport { TweetOption } from './tweet-option';\nimport { TweetShare } from './tweet-share';\nimport { ReactionType } from '@farcaster/hub-web';\nimport { useAuth } from '../../lib/context/auth-context';\n\ntype TweetStatsProps = Pick<\n  Tweet,\n  'userLikes' | 'userRetweets' | 'userReplies'\n> & {\n  reply?: boolean;\n  userId: string;\n  isOwner: boolean;\n  tweetId: string;\n  viewTweet?: boolean;\n  tweetAuthorId: string;\n  openModal?: () => void;\n};\n\nexport function TweetStats({\n  reply,\n  userId,\n  isOwner,\n  tweetId,\n  userLikes,\n  viewTweet,\n  userRetweets,\n  userReplies: totalReplies,\n  tweetAuthorId,\n  openModal\n}: TweetStatsProps): JSX.Element {\n  const { user } = useAuth();\n\n  const totalLikes = userLikes.length;\n  const totalRetweets = userRetweets.length;\n\n  const [currentStats, setCurrentStats] = useState({\n    currentReplies: totalReplies,\n    currentLikes: totalLikes,\n    currentRetweets: totalRetweets\n  });\n\n  const { currentReplies, currentRetweets, currentLikes } = currentStats;\n\n  useEffect(() => {\n    setCurrentStats({\n      currentReplies: totalReplies,\n      currentLikes: totalLikes,\n      currentRetweets: totalRetweets\n    });\n  }, [totalReplies, totalLikes, totalRetweets]);\n\n  const replyMove = useMemo(\n    () => (totalReplies > currentReplies ? -25 : 25),\n    [totalReplies]\n  );\n\n  const likeMove = useMemo(\n    () => (totalLikes > currentLikes ? -25 : 25),\n    [totalLikes]\n  );\n\n  const tweetMove = useMemo(\n    () => (totalRetweets > currentRetweets ? -25 : 25),\n    [totalRetweets]\n  );\n\n  const [tweetIsLiked, setTweetIsLiked] = useState(\n    user?.keyPair && userLikes.includes(userId)\n  );\n  const [tweetIsRetweeted, setTweetIsRetweeted] = useState(\n    user?.keyPair && userRetweets.includes(userId)\n  );\n\n  const isStatsVisible = !!(totalReplies || totalRetweets || totalLikes);\n\n  return (\n    <>\n      {viewTweet && (\n        <ViewTweetStats\n          likeMove={likeMove}\n          userLikes={userLikes}\n          tweetMove={tweetMove}\n          replyMove={replyMove}\n          userRetweets={userRetweets}\n          currentLikes={currentLikes}\n          currentTweets={currentRetweets}\n          currentReplies={currentReplies}\n          isStatsVisible={isStatsVisible}\n          tweetId={tweetId}\n        />\n      )}\n      <div\n        className={cn(\n          'flex text-light-secondary inner:outline-none dark:text-dark-secondary',\n          viewTweet ? 'justify-around py-2' : 'max-w-md justify-between'\n        )}\n      >\n        <TweetOption\n          className='hover:text-accent-blue focus-visible:text-accent-blue'\n          iconClassName='group-hover:bg-accent-blue/10 group-active:bg-accent-blue/20 \n                         group-focus-visible:bg-accent-blue/10 group-focus-visible:ring-accent-blue/80'\n          tip='Reply'\n          move={replyMove}\n          disabled={!!!user?.keyPair}\n          stats={currentReplies}\n          iconName='ChatBubbleOvalLeftIcon'\n          viewTweet={viewTweet}\n          onClick={openModal}\n        />\n        <TweetOption\n          className={cn(\n            'hover:text-accent-green focus-visible:text-accent-green',\n            tweetIsRetweeted && 'text-accent-green [&>i>svg]:[stroke-width:2px]'\n          )}\n          iconClassName='group-hover:bg-accent-green/10 group-active:bg-accent-green/20\n                         group-focus-visible:bg-accent-green/10 group-focus-visible:ring-accent-green/80'\n          tip={tweetIsRetweeted ? 'Undo Recast' : 'Recast'}\n          move={tweetMove}\n          disabled={!!!user?.keyPair}\n          stats={currentRetweets}\n          iconName='ArrowPathRoundedSquareIcon'\n          viewTweet={viewTweet}\n          onClick={async () => {\n            const message = await createReactionMessage({\n              castHash: tweetId,\n              castAuthorFid: parseInt(tweetAuthorId),\n              fid: parseInt(userId),\n              type: ReactionType.RECAST,\n              remove: tweetIsRetweeted\n            });\n            if (!message) {\n              console.error('Error creating recast message');\n              return;\n            }\n\n            const beforeStats = { ...currentStats };\n            const beforeTweetIsRetweeted = tweetIsRetweeted;\n\n            setCurrentStats({\n              currentReplies,\n              currentLikes,\n              currentRetweets: tweetIsRetweeted\n                ? totalRetweets - 1\n                : totalRetweets + 1\n            });\n            setTweetIsRetweeted(!tweetIsRetweeted);\n\n            const result = await submitHubMessage(message);\n\n            if (!result?.hash) {\n              setCurrentStats(beforeStats);\n              setTweetIsRetweeted(beforeTweetIsRetweeted);\n            }\n          }}\n        />\n        <TweetOption\n          className={cn(\n            'hover:text-accent-pink focus-visible:text-accent-pink',\n            tweetIsLiked && 'text-accent-pink [&>i>svg]:fill-accent-pink'\n          )}\n          iconClassName='group-hover:bg-accent-pink/10 group-active:bg-accent-pink/20\n                         group-focus-visible:bg-accent-pink/10 group-focus-visible:ring-accent-pink/80'\n          tip={tweetIsLiked ? 'Unlike' : 'Like'}\n          move={likeMove}\n          disabled={!!!user?.keyPair}\n          stats={currentLikes}\n          iconName='HeartIcon'\n          viewTweet={viewTweet}\n          onClick={async () => {\n            const message = await createReactionMessage({\n              castHash: tweetId,\n              castAuthorFid: parseInt(tweetAuthorId),\n              fid: parseInt(userId),\n              type: ReactionType.LIKE,\n              remove: tweetIsLiked\n            });\n            if (!message) {\n              console.error('Error creating like message');\n              return;\n            }\n            const beforeStats = { ...currentStats };\n            const beforeTweetIsLiked = tweetIsLiked;\n\n            setCurrentStats({\n              currentReplies,\n              currentRetweets: currentRetweets,\n              currentLikes: tweetIsLiked ? totalLikes - 1 : totalLikes + 1\n            });\n            setTweetIsLiked(!tweetIsLiked);\n\n            const result = await submitHubMessage(message);\n\n            if (!result?.hash) {\n              setCurrentStats(beforeStats);\n              setTweetIsLiked(beforeTweetIsLiked);\n            }\n          }}\n        />\n        <TweetShare userId={userId} tweetId={tweetId} viewTweet={viewTweet} />\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-status.tsx",
    "content": "import { motion } from 'framer-motion';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { CustomIcon } from '@components/ui/custom-icon';\nimport { fromTop } from '@components/input/input-form';\nimport type { ReactNode } from 'react';\n\ntype TweetStatusProps = {\n  type: 'pin' | 'tweet';\n  children: ReactNode;\n};\n\nexport function TweetStatus({ type, children }: TweetStatusProps): JSX.Element {\n  return (\n    <motion.div\n      className='col-span-2 grid grid-cols-[48px,1fr] gap-3 text-light-secondary dark:text-dark-secondary'\n      {...fromTop}\n    >\n      <i className='justify-self-end'>\n        {type === 'pin' ? (\n          <CustomIcon\n            className='h-5 w-5 -rotate-45 fill-light-secondary dark:fill-dark-secondary'\n            iconName='PinIcon'\n          />\n        ) : (\n          <HeroIcon className='h-5 w-5' iconName='ArrowPathRoundedSquareIcon' />\n        )}\n      </i>\n      {children}\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-text.tsx",
    "content": "import Link from 'next/link';\nimport { useMemo } from 'react';\nimport { ImagesPreview } from '../../lib/types/file';\nimport { Mention } from '../../lib/types/tweet';\nimport { replaceOccurrencesMultiple } from '../../lib/utils';\nimport { UserTooltip } from '../user/user-tooltip';\nimport isURL from 'validator/lib/isURL';\n\nexport interface TweetTextProps {\n  text: string;\n  images: ImagesPreview | null;\n  mentions: Mention[];\n}\n\nexport function splitAndInsert(\n  input: string,\n  indices: number[],\n  insertions: JSX.Element[],\n  elementBuilder: (s: string, key: any) => JSX.Element\n) {\n  let result = [];\n  let lastIndex = 0;\n\n  indices.forEach((index, i) => {\n    result.push(\n      elementBuilder(\n        Buffer.from(input).slice(lastIndex, index).toString(),\n        `el-${i}`\n      )\n    );\n    result.push(insertions[i]);\n    lastIndex = index;\n  });\n\n  result.push(\n    elementBuilder(\n      Buffer.from(input).slice(lastIndex).toString(),\n      `el-${indices.length}`\n    )\n  ); // get remaining part of string\n\n  return result;\n}\n\nexport function TweetText({\n  text,\n  images,\n  mentions\n}: TweetTextProps): JSX.Element {\n  const indices = useMemo(\n    () => mentions.map((mention) => mention.position),\n    [mentions]\n  );\n\n  const segments = useMemo(() => {\n    const segments = splitAndInsert(\n      text,\n      indices,\n      mentions.map((mention, index) => {\n        const link = (\n          <Link\n            href={`/user/${mention.username || mention.userId}`}\n            key={index}\n          >\n            <span className='inline text-main-accent hover:cursor-pointer hover:underline'>{`@${\n              mention.username || `!${mention.userId}`\n            }`}</span>\n          </Link>\n        );\n        return mention.user ? (\n          <span className='override-nav inline-block' key={index}>\n            <UserTooltip {...mention.user}>{link}</UserTooltip>\n          </span>\n        ) : (\n          link\n        );\n      }),\n      (s, index) => {\n        return (\n          <span className='inline' key={index}>\n            {s.split(/(\\s|\\n)/).map((part, index_) => {\n              if (isURL(part, { require_protocol: false })) {\n                return (\n                  <Link\n                    href={\n                      !(\n                        part.toLowerCase().startsWith('https://') ||\n                        part.toLowerCase().startsWith('http://')\n                      )\n                        ? `https://${part}`\n                        : part\n                    }\n                    key={index_}\n                    className='inline text-main-accent hover:cursor-pointer hover:underline'\n                    target='_blank'\n                  >\n                    {part}\n                  </Link>\n                );\n              } else {\n                return replaceOccurrencesMultiple(\n                  part,\n                  images?.map((img) => img.src ?? '') ?? [],\n                  ''\n                );\n              }\n            })}\n          </span>\n        );\n      }\n    );\n    return segments;\n  }, [text, indices, mentions, images]);\n\n  return <div className='whitespace-pre-line break-words'>{segments}</div>;\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-topic.tsx",
    "content": "import Link from 'next/link';\nimport useSWR from 'swr';\nimport { fetchJSON } from '../../lib/fetch';\nimport { TopicResponse, TopicType } from '../../lib/types/topic';\nimport { NextImage } from '../ui/next-image';\n\nexport function TweetTopicLazy({ topicUrl }: { topicUrl: string }) {\n  const { data, isValidating } = useSWR(\n    `/api/topic?url=${encodeURIComponent(topicUrl)}`,\n    async (url) => (await fetchJSON<TopicResponse>(url)).result\n  );\n\n  return !data ? (\n    isValidating ? (\n      <TweetTopicSkeleton />\n    ) : (\n      <></>\n    )\n  ) : (\n    <TweetTopic topic={data} />\n  );\n}\n\nexport function TweetTopicSkeleton() {\n  return (\n    <div className='flex animate-pulse items-center text-light-secondary dark:text-dark-secondary'>\n      #{' '}\n      <span className='ml-2 mr-1 h-4 w-10 flex-shrink-0 flex-grow-0 rounded-md bg-light-secondary dark:bg-dark-secondary'></span>\n    </div>\n  );\n}\n\nexport function TweetTopic({ topic }: { topic: TopicType }) {\n  return (\n    <Link\n      href={`/topic?url=${encodeURIComponent(topic.url)}`}\n      className='flex w-full cursor-pointer items-center whitespace-nowrap text-light-secondary hover:underline dark:text-dark-secondary'\n    >\n      <TopicView topic={topic}></TopicView>\n    </Link>\n  );\n}\n\nexport function TopicView({ topic }: { topic: TopicType }) {\n  return (\n    <div className='override-nav inline flex items-center gap-[2px]'>\n      <span>#</span>\n      {topic.image && (\n        <span className='mx-1 inline flex-shrink-0 flex-grow-0 overflow-hidden rounded-md'>\n          <NextImage\n            src={topic.image}\n            alt={topic.name}\n            objectFit='contain'\n            width={16}\n            height={16}\n          ></NextImage>\n        </span>\n      )}\n      <div className='inline inline overflow-hidden text-ellipsis'>\n        {/* TODO: Fix CSS truncation */}\n        {topic.name.length > 30 ? topic.name.slice(0, 30) + '...' : topic.name}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-with-parent.tsx",
    "content": "import type { Tweet as TweetType } from '@lib/types/tweet';\nimport { useState } from 'react';\nimport { Tweet } from './tweet';\nimport { TweetParent } from './tweet-parent';\n\ntype TweetWithParentProps = {\n  data: TweetType[];\n};\n\nexport type LoadedParents = Record<'parentId' | 'childId', string>[];\n\nexport function TweetWithParent({ data }: TweetWithParentProps): JSX.Element {\n  const [loadedParents, setLoadedParents] = useState<LoadedParents>([]);\n\n  const addParentId = (parentId: string, targetChildId: string): void =>\n    setLoadedParents((prevLoadedParents) =>\n      prevLoadedParents.some((item) => item.parentId === parentId)\n        ? prevLoadedParents\n        : [...prevLoadedParents, { parentId, childId: targetChildId }]\n    );\n\n  const filteredData = data.filter(\n    (child) => !loadedParents.some((parent) => parent.parentId === child.id)\n  );\n\n  return (\n    <>\n      {filteredData.map((tweet) => (\n        <div className='[&>article:nth-child(2)]:-mt-1' key={tweet.id}>\n          {tweet.parent && (\n            <TweetParent\n              parentId={tweet.parent.id}\n              loadedParents={loadedParents}\n              addParentId={addParentId}\n            />\n          )}\n          {tweet.user && <Tweet {...tweet} user={tweet.user} />}\n        </div>\n      ))}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet-with-parent.tsx.bak",
    "content": "import { useState } from 'react';\nimport { Tweet } from './tweet';\nimport { TweetParent } from './tweet-parent.tsx.bak';\nimport type { TweetWithUsers } from '@lib/types/tweet';\n\ntype TweetWithParentProps = {\n  data: TweetWithUsers[];\n};\n\nexport type LoadedParents = Record<'parentId' | 'childId', string>[];\n\nexport function TweetWithParent({ data }: TweetWithParentProps): JSX.Element {\n  const [loadedParents, setLoadedParents] = useState<LoadedParents>([]);\n\n  const addParentId = (parentId: string, targetChildId: string): void =>\n    setLoadedParents((prevLoadedParents) =>\n      prevLoadedParents.some((item) => item.parentId === parentId)\n        ? prevLoadedParents\n        : [...prevLoadedParents, { parentId, childId: targetChildId }]\n    );\n\n  const filteredData = data.filter(\n    (child) => !loadedParents.some((parent) => parent.parentId === child.id)\n  );\n\n  return (\n    <>\n      {filteredData.map((tweet) => (\n        <div className='[&>article:nth-child(2)]:-mt-1' key={tweet.id}>\n          {tweet.parent && (\n            <TweetParent\n              parentId={tweet.parent.id}\n              loadedParents={loadedParents}\n              addParentId={addParentId}\n            />\n          )}\n          <Tweet {...tweet} />\n        </div>\n      ))}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/tweet/tweet.tsx",
    "content": "import { ImagePreview } from '@components/input/image-preview';\nimport { Modal } from '@components/modal/modal';\nimport { TweetReplyModal } from '@components/modal/tweet-reply-modal';\nimport { UserAvatar } from '@components/user/user-avatar';\nimport { UserName } from '@components/user/user-name';\nimport { UserTooltip } from '@components/user/user-tooltip';\nimport { UserUsername } from '@components/user/user-username';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport type { Tweet } from '@lib/types/tweet';\nimport type { User, UsersMapType } from '@lib/types/user';\nimport cn from 'clsx';\nimport type { Variants } from 'framer-motion';\nimport Link from 'next/link';\nimport { useRouter } from 'next/router';\nimport { TweetActions } from './tweet-actions';\nimport { TweetDate } from './tweet-date';\nimport { TweetEmbeds } from './tweet-embed';\nimport { TweetStats } from './tweet-stats';\nimport { TweetText } from './tweet-text';\nimport { TweetTopicLazy } from './tweet-topic';\nimport { hasAncestorWithClass } from '../../lib/utils';\n\nexport type TweetProps = Tweet & {\n  user: User;\n  usersMap?: UsersMapType<User>;\n  modal?: boolean;\n  pinned?: boolean;\n  profile?: User | null;\n  parentTweet?: boolean;\n  quoted?: boolean;\n};\n\nexport const variants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1, transition: { duration: 0.8 } },\n  exit: { opacity: 0, transition: { duration: 0.2 } }\n};\n\nexport function Tweet(tweet: TweetProps): JSX.Element {\n  const {\n    id: tweetId,\n    text,\n    modal,\n    images,\n    parent,\n    pinned,\n    profile,\n    userLikes,\n    createdBy,\n    createdAt,\n    deletedAt,\n    parentTweet,\n    userReplies,\n    userRetweets,\n    mentions,\n    topic,\n    topicUrl,\n    retweet,\n    embeds,\n    user: tweetUserData,\n    quoted\n  } = tweet;\n\n  const { id: ownerId, name, username, verified, photoURL } = tweetUserData;\n  const { user } = useAuth();\n  const { open, openModal, closeModal } = useModal();\n  const tweetLink = `/tweet/${tweetId}`;\n  const userId = user?.id as string;\n  const isOwner = userId === createdBy;\n  const { id: parentId, username: parentUsername } = parent ?? {};\n  const { push } = useRouter();\n  const reply = !!parent;\n  const tweetIsRetweeted = retweet !== null;\n\n  return (\n    <article>\n      <Modal\n        className='flex items-start justify-center'\n        modalClassName='bg-main-background rounded-2xl max-w-xl w-full my-8 overflow-hidden'\n        open={open}\n        closeModal={closeModal}\n      >\n        <TweetReplyModal tweet={tweet} closeModal={closeModal} />\n      </Modal>\n      <div\n        className={cn(\n          `accent-tab sm:hover-card relative flex cursor-pointer \n             flex-col gap-y-4 px-4 py-3 outline-none duration-200`,\n          parentTweet\n            ? 'mt-0.5 pb-0 pt-2.5'\n            : 'border-b border-light-border dark:border-dark-border',\n          quoted &&\n            'mt-4 rounded-md border border-light-border p-4 dark:border-dark-border'\n        )}\n        onClick={(event) => {\n          const clickedElement = event.target as any;\n          // Prevent click when clicking on a link or a paragraph or image\n          const tagName = (event.target as any).tagName;\n          // DIV clicks do not propagate to parent, span used for body text\n          const isSpecialElement =\n            clickedElement.tagName === 'A' || // For links\n            clickedElement.tagName === 'IMG' || // For images\n            clickedElement.classList.contains('override-nav') ||\n            hasAncestorWithClass(clickedElement, 'override-nav');\n\n          // Prevent click when selecting text\n          const text = window.getSelection()?.toString();\n          if (text) {\n            return;\n          }\n\n          event.stopPropagation();\n\n          if (!isSpecialElement) {\n            push(tweetLink);\n          }\n        }}\n      >\n        <div className='grid grid-cols-[auto,1fr] gap-x-3 gap-y-1'>\n          <div className='flex flex-col items-center gap-2'>\n            <UserTooltip avatar modal={modal} {...tweetUserData}>\n              <UserAvatar src={photoURL} alt={name} username={username} />\n            </UserTooltip>\n            {parentTweet && (\n              <i className='hover-animation h-full w-0.5 bg-light-line-reply dark:bg-dark-line-reply' />\n            )}\n          </div>\n          <div className='flex min-w-0 flex-col'>\n            <div className='flex justify-between gap-2 text-light-secondary dark:text-dark-secondary'>\n              <div className='flex gap-1 truncate xs:overflow-visible xs:whitespace-normal'>\n                <UserTooltip modal={modal} {...tweetUserData}>\n                  <UserName\n                    name={name}\n                    username={username}\n                    verified={verified}\n                    className='text-light-primary dark:text-dark-primary'\n                  />\n                </UserTooltip>\n                <UserTooltip modal={modal} {...tweetUserData}>\n                  <UserUsername username={username} />\n                </UserTooltip>\n                <TweetDate tweetLink={tweetLink} createdAt={createdAt} />\n                {deletedAt && (\n                  <span className='text-light-secondary dark:text-dark-secondary'>\n                    · Deleted\n                  </span>\n                )}\n              </div>\n              <div className='px-4'>\n                {!modal && !quoted && (\n                  <TweetActions\n                    isOwner={isOwner}\n                    ownerId={ownerId}\n                    tweetId={tweetId}\n                    parentId={parentId}\n                    username={username}\n                    hasImages={!!images}\n                    createdBy={createdBy}\n                  />\n                )}\n              </div>\n            </div>\n            {(reply || modal) && parentUsername && (\n              <p\n                className={cn(\n                  'text-light-secondary dark:text-dark-secondary',\n                  modal && 'order-1 my-2'\n                )}\n              >\n                Replying to{' '}\n                <Link\n                  href={`/user/${parentUsername}`}\n                  className='custom-underline text-main-accent'\n                >\n                  @{parentUsername}\n                </Link>\n              </p>\n            )}\n            <div\n              className={cn(\n                'whitespace-pre-line break-words',\n                deletedAt\n                  ? 'text-light-secondary dark:text-dark-secondary'\n                  : undefined\n              )}\n            >\n              <TweetText\n                text={text || ''}\n                images={images}\n                mentions={mentions}\n              />\n            </div>\n            <div className='mt-1 flex flex-col gap-2'>\n              {images && (\n                <ImagePreview\n                  tweet\n                  imagesPreview={images}\n                  previewCount={images.length}\n                />\n              )}\n              {embeds && embeds.length > 0 && (\n                <TweetEmbeds\n                  embeds={embeds}\n                  tweetAuthorId={tweet.createdBy}\n                  tweetId={tweet.id}\n                />\n              )}\n              {tweet.usersMap &&\n                tweet.quoteTweets?.map((quoteTweet) => (\n                  <Tweet\n                    key={quoteTweet.id}\n                    {...quoteTweet}\n                    user={tweet.usersMap![quoteTweet.createdBy]}\n                    modal={modal}\n                    profile={profile}\n                    parentTweet={false}\n                    quoted\n                  />\n                ))}\n              {topicUrl && <TweetTopicLazy topicUrl={topicUrl} />}\n              {!modal && !quoted && (\n                <TweetStats\n                  reply={reply}\n                  userId={userId}\n                  isOwner={isOwner}\n                  tweetId={tweetId}\n                  userLikes={userLikes}\n                  userReplies={userReplies}\n                  userRetweets={userRetweets}\n                  tweetAuthorId={ownerId}\n                  openModal={openModal}\n                />\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/button.tsx",
    "content": "import { forwardRef } from 'react';\nimport cn from 'clsx';\nimport { Loading } from './loading';\nimport type { ComponentPropsWithRef } from 'react';\n\ntype ButtonProps = ComponentPropsWithRef<'button'> & {\n  loading?: boolean;\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, loading, disabled, children, ...rest }, ref) => {\n    const isDisabled = loading || disabled;\n\n    return (\n      <button\n        className={cn(\n          'custom-button main-tab',\n          loading && 'relative !text-transparent disabled:cursor-wait',\n          className\n        )}\n        type='button'\n        disabled={isDisabled}\n        ref={ref}\n        {...rest}\n      >\n        {loading && (\n          <Loading\n            iconClassName='h-5 w-5'\n            className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'\n          />\n        )}\n        {children}\n      </button>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/ui/caution-warn.tsx",
    "content": "import cn from 'clsx';\nimport { HeroIcon } from './hero-icon';\n\nexport function CautionWarn(): JSX.Element {\n  return (\n    <div\n      className='accent-tab relative \n   flex flex-col gap-0.5 border border-accent-yellow bg-accent-yellow bg-opacity-10 dark:border-dark-border lg:px-6 lg:py-4'\n    >\n      <div className={cn('flex items-center')}>\n        <div className={cn('mr-4 overflow-hidden text-accent-yellow')}>\n          <HeroIcon\n            className={cn('h-6 w-6')}\n            iconName={'ExclamationTriangleIcon'}\n          />\n        </div>\n        <div>\n          <p className='font-bold'>Caution</p>\n          <p className='text-sm'>\n            This feature is still being tested, proceed with caution.\n            Functionality and data formats are subject to change.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/custom-icon.tsx",
    "content": "import cn from 'clsx';\n\ntype IconName = keyof typeof Icons;\n\ntype IconProps = {\n  className?: string;\n};\n\ntype CustomIconProps = IconProps & {\n  iconName: IconName;\n};\n\nconst Icons = {\n  PinIcon,\n  AppleIcon,\n  PinOffIcon,\n  GoogleIcon,\n  TwitterIcon,\n  FeatherIcon,\n  SpinnerIcon,\n  TriangleIcon\n};\n\nexport function CustomIcon({\n  iconName,\n  className\n}: CustomIconProps): JSX.Element {\n  const Icon = Icons[iconName];\n\n  return <Icon className={className ?? 'h-6 w-6'} />;\n}\n\nfunction TwitterIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg className={cn('fill-current', className)} viewBox='-1 -1 24 24'>\n      <g>\n        <path d='M20.3 18.75C20.6868 18.75 21 19.0576 21 19.4375V20.125H14V19.4375C14 19.0576 14.3132 18.75 14.7 18.75H20.3Z' />\n        <path d='M20.3001 18.7499V18.0625C20.3001 17.6825 19.9868 17.3749 19.6001 17.3749H15.4001C15.0133 17.3749 14.7001 17.6825 14.7001 18.0625V18.7499H20.3001Z' />\n        <path d='M17.5 0.875H3.5V3.625H17.5V0.875Z' />\n        <path d='M20.3 6.37499H0.7L0 3.625H21L20.3 6.37499Z' />\n        <path d='M19.6 6.375H15.4V17.375H19.6V6.375Z' />\n        <path d='M6.3 18.75C6.68675 18.75 7 19.0576 7 19.4375V20.125H0V19.4375C0 19.0576 0.31325 18.75 0.7 18.75H6.3Z' />\n        <path d='M6.30007 18.7499V18.0625C6.30007 17.6825 5.98682 17.3749 5.60007 17.3749H1.40007C1.01332 17.3749 0.700074 17.6825 0.700074 18.0625L0.700073 18.7499H6.30007Z' />\n        <path d='M5.60002 6.375H1.40002V17.375H5.60002V6.375Z' />\n        <path d='M5.59998 11.82C5.59998 9.16213 7.79378 7.0075 10.5 7.0075C13.2062 7.0075 15.4 9.16213 15.4 11.82V6.375H5.59998V11.82Z' />\n      </g>\n    </svg>\n  );\n}\n\nfunction FeatherIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg\n      className={cn('fill-current', className)}\n      viewBox='0 0 24 24'\n      aria-hidden='true'\n    >\n      <g>\n        <path d='M23 3c-6.62-.1-10.38 2.421-13.05 6.03C7.29 12.61 6 17.331 6 22h2c0-1.007.07-2.012.19-3H12c4.1 0 7.48-3.082 7.94-7.054C22.79 10.147 23.17 6.359 23 3zm-7 8h-1.5v2H16c.63-.016 1.2-.08 1.72-.188C16.95 15.24 14.68 17 12 17H8.55c.57-2.512 1.57-4.851 3-6.78 2.16-2.912 5.29-4.911 9.45-5.187C20.95 8.079 19.9 11 16 11zM4 9V6H1V4h3V1h2v3h3v2H6v3H4z' />\n      </g>\n    </svg>\n  );\n}\n\nfunction SpinnerIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg\n      className={cn('animate-spin', className)}\n      xmlns='http://www.w3.org/2000/svg'\n      fill='none'\n      viewBox='0 0 24 24'\n    >\n      <circle\n        className='opacity-25'\n        cx='12'\n        cy='12'\n        r='10'\n        stroke='currentColor'\n        strokeWidth='4'\n      />\n      <path\n        className='opacity-75'\n        fill='currentColor'\n        d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'\n      />\n    </svg>\n  );\n}\n\nfunction GoogleIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg\n      className={className}\n      version='1.1'\n      xmlns='http://www.w3.org/2000/svg'\n      viewBox='0 0 48 48'\n    >\n      <g>\n        <path\n          fill='#EA4335'\n          d='M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z'\n        />\n        <path\n          fill='#4285F4'\n          d='M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z'\n        />\n        <path\n          fill='#FBBC05'\n          d='M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z'\n        />\n        <path\n          fill='#34A853'\n          d='M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z'\n        />\n        <path fill='none' d='M0 0h48v48H0z' />\n      </g>\n    </svg>\n  );\n}\n\nfunction AppleIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg className={className} viewBox='0 0 24 24'>\n      <g>\n        <path d='M16.365 1.43c0 1.14-.493 2.27-1.177 3.08-.744.9-1.99 1.57-2.987 1.57-.12 0-.23-.02-.3-.03-.01-.06-.04-.22-.04-.39 0-1.15.572-2.27 1.206-2.98.804-.94 2.142-1.64 3.248-1.68.03.13.05.28.05.43zm4.565 15.71c-.03.07-.463 1.58-1.518 3.12-.945 1.34-1.94 2.71-3.43 2.71-1.517 0-1.9-.88-3.63-.88-1.698 0-2.302.91-3.67.91-1.377 0-2.332-1.26-3.428-2.8-1.287-1.82-2.323-4.63-2.323-7.28 0-4.28 2.797-6.55 5.552-6.55 1.448 0 2.675.95 3.6.95.865 0 2.222-1.01 3.902-1.01.613 0 2.886.06 4.374 2.19-.13.09-2.383 1.37-2.383 4.19 0 3.26 2.854 4.42 2.955 4.45z' />\n      </g>\n    </svg>\n  );\n}\n\nfunction TriangleIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg className={className} viewBox='0 0 24 24' aria-hidden='true'>\n      <g>\n        <path d='M12.538 6.478c-.14-.146-.335-.228-.538-.228s-.396.082-.538.228l-9.252 9.53c-.21.217-.27.538-.152.815.117.277.39.458.69.458h18.5c.302 0 .573-.18.69-.457.118-.277.058-.598-.152-.814l-9.248-9.532z' />\n      </g>\n    </svg>\n  );\n}\n\nfunction PinIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg\n      className={className}\n      xmlns='http://www.w3.org/2000/svg'\n      width='24'\n      height='24'\n      viewBox='0 0 24 24'\n      strokeWidth='2'\n      stroke='currentColor'\n      fill='none'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n    >\n      <path stroke='none' d='M0 0h24v24H0z' fill='none' />\n      <path d='M15 4.5l-4 4l-4 1.5l-1.5 1.5l7 7l1.5 -1.5l1.5 -4l4 -4' />\n      <line x1='9' y1='15' x2='4.5' y2='19.5' />\n      <line x1='14.5' y1='4' x2='20' y2='9.5' />\n    </svg>\n  );\n}\n\nfunction PinOffIcon({ className }: IconProps): JSX.Element {\n  return (\n    <svg\n      className={className}\n      xmlns='http://www.w3.org/2000/svg'\n      width='24'\n      height='24'\n      viewBox='0 0 24 24'\n      strokeWidth='2'\n      stroke='currentColor'\n      fill='none'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n    >\n      <path stroke='none' d='M0 0h24v24H0z' fill='none' />\n      <line x1='3' y1='3' x2='21' y2='21' />\n      <path d='M15 4.5l-3.249 3.249m-2.57 1.433l-2.181 .818l-1.5 1.5l7 7l1.5 -1.5l.82 -2.186m1.43 -2.563l3.25 -3.251' />\n      <line x1='9' y1='15' x2='4.5' y2='19.5' />\n      <line x1='14.5' y1='4' x2='20' y2='9.5' />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/error.tsx",
    "content": "import { HeroIcon } from './hero-icon';\n\ntype ErrorProps = {\n  message?: string;\n};\n\nexport function Error({ message }: ErrorProps): JSX.Element {\n  return (\n    <div\n      className='flex flex-col items-center justify-center \n                 gap-2 py-5 px-3 text-light-secondary dark:text-dark-secondary'\n    >\n      <i>\n        <HeroIcon className='h-10 w-10' iconName='ExclamationTriangleIcon' />\n      </i>\n      <p>{message ?? 'Something went wrong. Try Loading.'}</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/feed-ordering-selector.tsx",
    "content": "import { useAuth } from '../../lib/context/auth-context';\nimport { FeedOrderingType } from '../../lib/types/feed';\nimport { SegmentedNavLink } from './segmented-nav-link';\n\ninterface FeedOrderingSelectorProps {\n  feedOrdering: FeedOrderingType;\n  setFeedOrdering: (ordering: FeedOrderingType) => void;\n}\n\nexport function FeedOrderingSelector({\n  feedOrdering,\n  setFeedOrdering\n}: FeedOrderingSelectorProps) {\n  const { setTimelineCursor } = useAuth();\n\n  return (\n    // <div className='flex justify-between'>\n    <div\n      className='hover-animation flex justify-between overflow-y-auto\n    border-b border-light-border dark:border-dark-border'\n    >\n      {[\n        { name: 'Latest', value: 'latest' },\n        { name: 'Top', value: 'top' }\n      ].map((item) => (\n        <SegmentedNavLink\n          name={item.name}\n          key={item.value}\n          isActive={feedOrdering === item.value}\n          onClick={() => {\n            setFeedOrdering(item.value as FeedOrderingType);\n            setTimelineCursor(new Date());\n          }}\n        />\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/follow-button.tsx",
    "content": "import { ActionModal } from '@components/modal/action-modal';\nimport { Modal } from '@components/modal/modal';\nimport { Button } from '@components/ui/button';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport { preventBubbling } from '@lib/utils';\nimport { useState } from 'react';\nimport toast from 'react-hot-toast';\nimport {\n  createFollowMessage,\n  submitHubMessage\n} from '../../lib/farcaster/utils';\n\ntype FollowButtonProps = {\n  userTargetId: string;\n  userTargetUsername: string;\n};\n\nexport function FollowButton({\n  userTargetId,\n  userTargetUsername\n}: FollowButtonProps): JSX.Element | null {\n  const { user } = useAuth();\n  const { open, openModal, closeModal } = useModal();\n\n  if (user?.id === userTargetId) return null;\n\n  const { id: userId, following } = user ?? {};\n\n  const [userIsFollowed, setUserIsFollowed] = useState<boolean>(\n    user?.keyPair != undefined && !!following?.includes(userTargetId ?? '')\n  );\n\n  const handleFollow = async (): Promise<void> => {\n    if (!userId) {\n      toast.error(`Failed to follow @${userTargetUsername}`);\n    }\n\n    const message = await createFollowMessage({\n      fid: parseInt(userId!),\n      targetFid: parseInt(userTargetId),\n      remove: false\n    });\n\n    if (message) {\n      const res = await submitHubMessage(message);\n      if (res) {\n        setUserIsFollowed(true);\n      }\n    } else {\n      toast.error(`Failed to follow @${userTargetUsername}`);\n    }\n    closeModal();\n  };\n\n  const handleUnfollow = async (): Promise<void> => {\n    if (!userId) {\n      toast.error(`Failed to unfollow @${userTargetUsername}`);\n    }\n\n    const message = await createFollowMessage({\n      fid: parseInt(userId!),\n      targetFid: parseInt(userTargetId),\n      remove: true\n    });\n\n    if (message) {\n      const res = await submitHubMessage(message);\n      if (res) {\n        setUserIsFollowed(false);\n      }\n    } else {\n      toast.error(`Failed to unfollow @${userTargetUsername}`);\n    }\n    closeModal();\n  };\n\n  return (\n    <>\n      <Modal\n        modalClassName='flex flex-col gap-6 max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={open}\n        closeModal={closeModal}\n      >\n        <ActionModal\n          title={`Unfollow @${userTargetUsername}?`}\n          description='Their Tweets will no longer show up in your home timeline. You can still view their profile, unless their Tweets are protected.'\n          mainBtnLabel='Unfollow'\n          action={handleUnfollow}\n          closeModal={closeModal}\n        />\n      </Modal>\n      {userIsFollowed ? (\n        <Button\n          className='dark-bg-tab min-w-[106px] self-start border border-light-line-reply px-4 py-1.5 \n                     font-bold hover:border-accent-red hover:bg-accent-red/10 hover:text-accent-red\n                     hover:before:content-[\"Unfollow\"] inner:hover:hidden dark:border-light-secondary'\n          onClick={preventBubbling(openModal)}\n        >\n          <span>Following</span>\n        </Button>\n      ) : (\n        <Button\n          className='self-start border bg-light-primary px-4 py-1.5 font-bold text-white hover:bg-light-primary/90 \n                     focus-visible:bg-light-primary/90 active:bg-light-border/75 dark:bg-light-border \n                     dark:text-light-primary dark:hover:bg-light-border/90 dark:focus-visible:bg-light-border/90 \n                     dark:active:bg-light-border/75'\n          onClick={preventBubbling(handleFollow)}\n          disabled={!!!user?.keyPair}\n        >\n          Follow\n        </Button>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/hero-icon.tsx",
    "content": "import * as SolidIcons from '@heroicons/react/24/solid';\nimport * as OutlineIcons from '@heroicons/react/24/outline';\n\nexport type IconName = keyof typeof SolidIcons | keyof typeof OutlineIcons;\n\ntype HeroIconProps = {\n  solid?: boolean;\n  iconName: IconName;\n  className?: string;\n};\n\nexport function HeroIcon({\n  solid,\n  iconName,\n  className\n}: HeroIconProps): JSX.Element {\n  const Icon = solid ? SolidIcons[iconName] : OutlineIcons[iconName];\n\n  return <Icon className={className ?? 'h-6 w-6'} />;\n}\n"
  },
  {
    "path": "src/components/ui/loading.tsx",
    "content": "import cn from 'clsx';\nimport { CustomIcon } from './custom-icon';\n\ntype LoadingProps = {\n  className?: string;\n  iconClassName?: string;\n};\n\nexport function Loading({\n  className,\n  iconClassName\n}: LoadingProps): JSX.Element {\n  return (\n    <i className={cn('flex justify-center', className ?? 'p-4')}>\n      <CustomIcon\n        className={cn('text-main-accent', iconClassName ?? 'h-7 w-7')}\n        iconName='SpinnerIcon'\n      />\n    </i>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/menu-row.tsx",
    "content": "import Link from 'next/link';\nimport { HeroIcon, IconName } from './hero-icon';\nimport cn from 'clsx';\nimport { Loading } from './loading';\n\ntype MenuLinkPropsBase = {\n  description: string;\n  title: string;\n  iconName: IconName;\n  variant?: 'destructive' | 'primary';\n  isLoading?: boolean;\n};\n\ntype MenuLinkPropsLink = MenuLinkPropsBase & {\n  href: string;\n  onClick?: never;\n};\n\ntype MenuLinkPropsButton = MenuLinkPropsBase & {\n  href?: never;\n  onClick: (...args: unknown[]) => unknown;\n};\n\nexport type MenuLinkProps = MenuLinkPropsLink | MenuLinkPropsButton;\n\nfunction MenuRowBase({\n  description,\n  title,\n  iconName,\n  variant,\n  isLoading\n}: MenuLinkPropsBase) {\n  return (\n    <div\n      className='hover-animation accent-tab hover-card relative \n               flex cursor-pointer flex-col gap-0.5 border-b border-light-border dark:border-dark-border lg:px-6 lg:py-4'\n    >\n      <div\n        className={cn(\n          'flex items-center',\n          variant === 'destructive' ? 'text-accent-red' : undefined\n        )}\n      >\n        <div\n          className={cn(\n            'mr-4 overflow-hidden',\n            variant === 'primary' ? 'text-accent-green' : undefined\n          )}\n        >\n          <HeroIcon className={cn('h-6 w-6')} iconName={iconName} />\n        </div>\n        <div>\n          <p className='font-bold'>{title}</p>\n          <p className='text-sm text-light-secondary dark:text-dark-secondary'>\n            {description}\n          </p>\n        </div>\n        {isLoading && (\n          <div className='ml-auto'>\n            <Loading />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport function MenuRow(props: MenuLinkProps) {\n  const { href, onClick, isLoading, ...rest } = props;\n\n  return href ? (\n    <Link href={href}>\n      <MenuRowBase {...rest} />\n    </Link>\n  ) : (\n    <div onClick={() => !isLoading && onClick?.()}>\n      <MenuRowBase {...rest} isLoading={isLoading} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/next-image.tsx",
    "content": "import { useState } from 'react';\nimport Image from 'next/image';\nimport cn from 'clsx';\nimport type { ReactNode } from 'react';\nimport type { ImageProps } from 'next/image';\n\ntype NextImageProps = {\n  alt: string;\n  width?: string | number;\n  children?: ReactNode;\n  useSkeleton?: boolean;\n  imgClassName?: string;\n  previewCount?: number;\n  blurClassName?: string;\n} & ImageProps;\n\n/**\n *\n * @description Must set width and height, if not add layout='fill'\n * @param useSkeleton add background with pulse animation, don't use it if image is transparent\n */\nexport function NextImage({\n  src,\n  alt,\n  width,\n  height,\n  children,\n  className,\n  useSkeleton,\n  imgClassName,\n  previewCount,\n  blurClassName,\n  ...rest\n}: NextImageProps): JSX.Element {\n  const [loading, setLoading] = useState(!!useSkeleton);\n\n  const handleLoad = (): void => setLoading(false);\n\n  return (\n    <figure style={{ width, height }} className={cn(className, \"overflow-hidden\")}>\n      <Image\n        className={cn(\n          loading\n            ? blurClassName ??\n            'animate-pulse bg-light-secondary dark:bg-dark-secondary'\n            : previewCount === 1\n              ? 'h-full min-h-0 w-full min-w-0 rounded-lg object-contain'\n              : 'object-cover',\n          imgClassName,\n        )}\n        src={src}\n        width={width}\n        height={height}\n        alt={alt}\n        onLoadingComplete={handleLoad}\n        layout='responsive'\n        {...rest}\n      />\n      {children}\n    </figure>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/segmented-nav-link.tsx",
    "content": "import { useRouter } from 'next/router';\nimport Link from 'next/link';\nimport cn from 'clsx';\n\ntype SegmentedNavLinkProps = {\n  name: string;\n  path?: string;\n  onClick?: () => void;\n  isActive?: boolean;\n};\n\nexport function SegmentedNavLink({\n  name,\n  path = '#',\n  onClick,\n  isActive\n}: SegmentedNavLinkProps): JSX.Element {\n  const { asPath } = useRouter();\n\n  return (\n    <Link\n      href={path}\n      scroll={false}\n      className='hover-animation main-tab dark-bg-tab flex flex-1 justify-center\n                   hover:bg-light-primary/10 dark:hover:bg-dark-primary/10'\n    >\n      <div className='px-6 md:px-8' onClick={onClick}>\n        <p\n          className={cn(\n            'flex flex-col gap-3 whitespace-nowrap pt-3 font-bold transition-colors duration-200',\n            !!isActive || asPath === path\n              ? 'text-light-primary dark:text-dark-primary [&>i]:scale-100 [&>i]:opacity-100'\n              : 'text-light-secondary dark:text-dark-secondary'\n          )}\n        >\n          {name}\n          <i className='h-1 scale-50 rounded-full bg-main-accent opacity-0 transition duration-200' />\n        </p>\n      </div>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/tooltip.tsx",
    "content": "import cn from 'clsx';\n\ntype ToolTipProps = {\n  tip: string;\n  modal?: boolean;\n  className?: string;\n  groupInner?: boolean;\n};\n\nexport function ToolTip({\n  tip,\n  modal,\n  className,\n  groupInner\n}: ToolTipProps): JSX.Element | null {\n  if (modal) return null;\n\n  return (\n    <div\n      className={cn(\n        `invisible absolute left-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-[#666666] px-1 py-0.5 text-xs\n         text-white opacity-0 [transition:visibility_0ms_ease_200ms,opacity_200ms_ease] dark:bg-[#495A69]`,\n        groupInner\n          ? `group-hover/inner:visible group-hover/inner:opacity-100 group-hover/inner:delay-500 \n             group-focus-visible/inner:visible group-focus-visible/inner:opacity-100 group-focus-visible/inner:delay-200`\n          : `group-hover:visible group-hover:opacity-100 group-hover:delay-500 group-focus-visible:visible \n             group-focus-visible:opacity-100`,\n        className ?? 'translate-y-3'\n      )}\n    >\n      <span>{tip}</span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-avatar.tsx",
    "content": "import Link from 'next/link';\nimport cn from 'clsx';\nimport { NextImage } from '@components/ui/next-image';\n\ntype UserAvatarProps = {\n  src: string;\n  alt: string;\n  size?: number;\n  username?: string;\n  className?: string;\n};\n\nexport function UserAvatar({\n  src,\n  alt,\n  size,\n  username,\n  className\n}: UserAvatarProps): JSX.Element {\n  const pictureSize = size ?? 48;\n\n  return (\n    <Link href={username ? `/user/${username}` : '#'}>\n      <div\n        className={cn(\n          'blur-picture override-nav border border-gray-200 dark:border-gray-800',\n          !username && 'pointer-events-none',\n          className\n        )}\n        tabIndex={username ? 0 : -1}\n      >\n        <NextImage\n          useSkeleton\n          className='overflow-hidden rounded-full'\n          imgClassName='rounded-full !h-full !w-full'\n          width={pictureSize}\n          height={pictureSize}\n          src={src}\n          alt={alt}\n          key={src}\n        />\n      </div>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-card.tsx",
    "content": "import Link from 'next/link';\nimport { UserAvatar } from '@components/user/user-avatar';\nimport { FollowButton } from '@components/ui/follow-button';\nimport { UserTooltip } from './user-tooltip';\nimport { UserName } from './user-name';\nimport { UserFollowing } from './user-following';\nimport { UserUsername } from './user-username';\nimport type { User } from '@lib/types/user';\nimport { TweetText } from '../tweet/tweet-text';\nimport { UserFid } from './user-fid';\n\ntype UserCardProps = User & {\n  modal?: boolean;\n  follow?: boolean;\n};\n\nexport function UserCard(user: UserCardProps): JSX.Element {\n  const { id, bio, name, modal, follow, username, verified, photoURL } = user;\n\n  return (\n    <Link\n      href={`/user/${username}`}\n      className='accent-tab hover-animation grid grid-cols-[auto,1fr] gap-3 px-4\n                   py-3 hover:bg-light-primary/5 dark:hover:bg-dark-primary/5'\n    >\n      <UserTooltip avatar {...user} modal={modal}>\n        <UserAvatar src={photoURL} alt={name} username={username} />\n      </UserTooltip>\n      <div className='flex flex-col gap-1 truncate xs:overflow-visible'>\n        <div className='flex items-center justify-between gap-2 truncate xs:overflow-visible'>\n          <div className='flex flex-col justify-center truncate xs:overflow-visible xs:whitespace-normal'>\n            <UserTooltip {...user} modal={modal}>\n              <UserName\n                className='-mb-1'\n                name={name}\n                username={username}\n                verified={verified}\n              />\n            </UserTooltip>\n            <div className='flex items-center gap-1 text-light-secondary dark:text-dark-secondary'>\n              <UserTooltip {...user} modal={modal}>\n                <UserUsername username={username} />\n              </UserTooltip>\n              <UserFid userId={id} />\n              {follow && <UserFollowing userTargetId={id} />}\n            </div>\n          </div>\n          <FollowButton userTargetId={id} userTargetUsername={username} />\n        </div>\n        {follow && bio && <TweetText text={bio} mentions={[]} images={[]} />}\n      </div>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-cards.tsx",
    "content": "import cn from 'clsx';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { StatsEmpty } from '@components/tweet/stats-empty';\nimport { Loading } from '@components/ui/loading';\nimport { variants } from '@components/user/user-header';\nimport { UserCard } from './user-card';\nimport type { User } from '@lib/types/user';\nimport type { StatsType } from '@components/view/view-tweet-stats';\nimport type { StatsEmptyProps } from '@components/tweet/stats-empty';\n\ntype FollowType = 'following' | 'followers';\n\ntype CombinedTypes = StatsType | FollowType;\n\ntype UserCardsProps = {\n  data: User[] | null;\n  type: CombinedTypes;\n  follow?: boolean;\n  loading: boolean;\n  LoadMore?: () => JSX.Element;\n};\n\ntype NoStatsData = Record<CombinedTypes, StatsEmptyProps>;\n\nconst allNoStatsData: Readonly<NoStatsData> = {\n  retweets: {\n    title: 'Amplify Casts you like',\n    imageData: { src: '/assets/no-retweets.png', alt: 'No recasts' },\n    description:\n      'Share someone else’s Cast on your timeline by Retweeting it. When you do, it’ll show up here.'\n  },\n  likes: {\n    title: 'No Cast Likes yet',\n    imageData: { src: '/assets/no-likes.png', alt: 'No likes' },\n    description: 'When you like a Cast, it’ll show up here.'\n  },\n  following: {\n    title: 'Be in the know',\n    description:\n      'Following accounts is an easy way to curate your timeline and know what’s happening with the topics and people you’re interested in.'\n  },\n  followers: {\n    title: 'Looking for followers?',\n    imageData: { src: '/assets/no-followers.png', alt: 'No followers' },\n    description:\n      'When someone follows this account, they’ll show up here. Tweeting and interacting with others helps boost followers.'\n  }\n};\n\nexport function UserCards({\n  data,\n  type,\n  follow,\n  loading,\n  LoadMore\n}: UserCardsProps): JSX.Element {\n  const noStatsData = allNoStatsData[type];\n  const modal = ['retweets', 'likes'].includes(type);\n\n  return (\n    <section\n      className={cn(\n        modal && 'h-full overflow-y-auto [&>div:first-child>a]:mt-[52px]',\n        loading && 'flex items-center justify-center'\n      )}\n    >\n      {loading ? (\n        <Loading className={modal ? 'mt-[52px]' : 'mt-5'} />\n      ) : (\n        // <AnimatePresence mode='popLayout'>\n        <div className='mt-10'>\n          {data?.length ? (\n            data.map((userData) => (\n              <div key={userData.id}>\n                <UserCard {...userData} follow={follow} modal={modal} />\n              </div>\n            ))\n          ) : (\n            <StatsEmpty {...noStatsData} modal={modal} />\n          )}\n          {LoadMore && <LoadMore />}\n        </div>\n        // </AnimatePresence>\n      )}\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-details.tsx",
    "content": "import type { IconName } from '@components/ui/hero-icon';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { formatDate } from '@lib/date';\nimport type { UserFull } from '@lib/types/user';\nimport Link from 'next/link';\nimport { useAuth } from '../../lib/context/auth-context';\nimport { TweetText } from '../tweet/tweet-text';\nimport { TopicView } from '../tweet/tweet-topic';\nimport { UserFid } from './user-fid';\nimport { UserFollowStats } from './user-follow-stats';\nimport { UserFollowing } from './user-following';\nimport { UserKnownFollowersLazy } from './user-known-followers';\nimport { UserName } from './user-name';\n\ntype UserDetailsProps = Pick<\n  UserFull,\n  | 'id'\n  | 'bio'\n  | 'name'\n  | 'website'\n  | 'username'\n  | 'location'\n  | 'verified'\n  | 'createdAt'\n  | 'following'\n  | 'followers'\n  | 'interests'\n>;\n\ntype DetailIcon = [string | null, IconName];\n\nexport function UserDetails({\n  id,\n  bio,\n  name,\n  website,\n  username,\n  location,\n  verified,\n  createdAt,\n  following,\n  followers,\n  interests\n}: UserDetailsProps): JSX.Element {\n  const detailIcons: Readonly<DetailIcon[]> = [\n    [location, 'MapPinIcon'],\n    [website, 'LinkIcon']\n    // [`Joined ${formatDate(new Date(createdAt), 'joined')}`, 'CalendarDaysIcon']\n  ];\n  const { user: currentUser } = useAuth();\n\n  return (\n    <>\n      <div>\n        <UserName\n          className='-mb-1 text-xl'\n          name={name}\n          iconClassName='w-6 h-6'\n          verified={verified}\n        />\n        <div className='flex items-center gap-1 text-light-secondary dark:text-dark-secondary'>\n          <p>@{username}</p>\n          <UserFid userId={id} />\n          <UserFollowing userTargetId={id} />\n        </div>\n      </div>\n      <div className='flex flex-col gap-2'>\n        {/* {bio && <p className='whitespace-pre-line break-words'>{bio}</p>} */}\n        {bio && <TweetText text={bio} images={[]} mentions={[]} />}\n        <div className='flex flex-wrap gap-x-3 gap-y-1 text-light-secondary dark:text-dark-secondary'>\n          {detailIcons.map(\n            ([detail, icon], index) =>\n              detail && (\n                <div className='flex items-center gap-1' key={icon}>\n                  <i>\n                    <HeroIcon className='h-5 w-5' iconName={icon} />\n                  </i>\n                  {index === 1 ? (\n                    <a\n                      className='custom-underline text-main-accent'\n                      href={`https://${detail}`}\n                      target='_blank'\n                      rel='noreferrer'\n                    >\n                      {detail}\n                    </a>\n                  ) : index === 2 ? (\n                    <button className='custom-underline group relative'>\n                      {detail}\n                      <ToolTip\n                        className='translate-y-1'\n                        tip={formatDate(createdAt, 'full')}\n                      />\n                    </button>\n                  ) : (\n                    <p>{detail}</p>\n                  )}\n                </div>\n              )\n          )}\n        </div>\n      </div>\n      <div className='flex flex-wrap'>\n        {interests.map((topic) => (\n          <Link\n            href={`/topic?url=${topic.url}`}\n            key={topic.url}\n            className='cursor-pointer pr-2 text-light-secondary hover:underline dark:text-dark-secondary'\n          >\n            <TopicView topic={topic} />\n          </Link>\n        ))}\n      </div>\n      <UserFollowStats following={following} followers={followers} />\n      {currentUser?.keyPair && currentUser?.id !== id && (\n        <UserKnownFollowersLazy userId={id} />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-edit-profile.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { toast } from 'react-hot-toast';\nimport cn from 'clsx';\nimport { useUser } from '@lib/context/user-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport { sleep } from '@lib/utils';\nimport { getImagesData } from '@lib/validation';\nimport { Modal } from '@components/modal/modal';\nimport { EditProfileModal } from '@components/modal/edit-profile-modal';\nimport { Button } from '@components/ui/button';\nimport { InputField } from '@components/input/input-field';\nimport type { ChangeEvent, KeyboardEvent } from 'react';\nimport type { FilesWithId } from '@lib/types/file';\nimport type {\n  User,\n  EditableData,\n  EditableUserData,\n  UserFull\n} from '@lib/types/user';\nimport type { InputFieldProps } from '@components/input/input-field';\n\ntype RequiredInputFieldProps = Omit<InputFieldProps, 'handleChange'> & {\n  inputId: EditableData;\n};\n\ntype UserImages = Record<\n  Extract<EditableData, 'photoURL' | 'coverPhotoURL'>,\n  FilesWithId\n>;\n\ntype TrimmedTexts = Pick<\n  EditableUserData,\n  Exclude<EditableData, 'photoURL' | 'coverPhotoURL'>\n>;\n\ntype UserEditProfileProps = {\n  hide?: boolean;\n};\n\nexport function UserEditProfile({ hide }: UserEditProfileProps): JSX.Element {\n  const { user } = useUser();\n  const { open, openModal, closeModal } = useModal();\n\n  const [loading, setLoading] = useState(false);\n\n  const { bio, name, website, location, photoURL, coverPhotoURL } =\n    user as UserFull;\n\n  const [editUserData, setEditUserData] = useState<EditableUserData>({\n    bio,\n    name,\n    website,\n    photoURL,\n    location,\n    coverPhotoURL\n  });\n\n  const [userImages, setUserImages] = useState<UserImages>({\n    photoURL: [],\n    coverPhotoURL: []\n  });\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useEffect(() => cleanImage, []);\n\n  const inputNameError = !editUserData.name?.trim()\n    ? \"Name can't be blank\"\n    : '';\n\n  const updateData = async (): Promise<void> => {\n    setLoading(true);\n\n    const userId = user?.id as string;\n\n    const { photoURL, coverPhotoURL: coverURL } = userImages;\n\n    // const [newPhotoURL, newCoverPhotoURL] = await Promise.all(\n    //   [photoURL, coverURL].map((image) => uploadImages(userId, image))\n    // );\n\n    // const newImages: Partial<Pick<User, 'photoURL' | 'coverPhotoURL'>> = {\n    //   coverPhotoURL:\n    //     coverPhotoURL === editUserData.coverPhotoURL\n    //       ? coverPhotoURL\n    //       : newCoverPhotoURL?.[0].src ?? null,\n    //   ...(newPhotoURL && { photoURL: newPhotoURL[0].src })\n    // };\n\n    const trimmedKeys: Readonly<EditableData[]> = [\n      'name',\n      'bio',\n      'location',\n      'website'\n    ];\n\n    const trimmedTexts = trimmedKeys.reduce(\n      (acc, curr) => ({ ...acc, [curr]: editUserData[curr]?.trim() ?? null }),\n      {} as TrimmedTexts\n    );\n\n    // const newUserData: Readonly<EditableUserData> = {\n    //   ...editUserData,\n    //   ...trimmedTexts,\n    //   ...newImages\n    // };\n\n    await sleep(500);\n\n    // await updateUserData(userId, newUserData);\n\n    closeModal();\n\n    cleanImage();\n\n    setLoading(false);\n    // setEditUserData(newUserData);\n\n    toast.success('Profile updated successfully');\n  };\n\n  const editImage =\n    (type: 'cover' | 'profile') =>\n    ({ target: { files } }: ChangeEvent<HTMLInputElement>): void => {\n      const imagesData = getImagesData(files);\n\n      if (!imagesData) {\n        toast.error('Please choose a valid GIF or Photo');\n        return;\n      }\n\n      const { imagesPreviewData, selectedImagesData } = imagesData;\n\n      const targetKey = type === 'cover' ? 'coverPhotoURL' : 'photoURL';\n      const newImage = imagesPreviewData[0].src;\n\n      setEditUserData({\n        ...editUserData,\n        [targetKey]: newImage\n      });\n\n      setUserImages({\n        ...userImages,\n        [targetKey]: selectedImagesData\n      });\n    };\n\n  const removeCoverImage = (): void => {\n    setEditUserData({\n      ...editUserData,\n      coverPhotoURL: null\n    });\n\n    setUserImages({\n      ...userImages,\n      coverPhotoURL: []\n    });\n\n    URL.revokeObjectURL(editUserData.coverPhotoURL ?? '');\n  };\n\n  const cleanImage = (): void => {\n    const imagesKey: Readonly<Partial<EditableData>[]> = [\n      'photoURL',\n      'coverPhotoURL'\n    ];\n\n    imagesKey.forEach((image) =>\n      URL.revokeObjectURL(editUserData[image] ?? '')\n    );\n\n    setUserImages({\n      photoURL: [],\n      coverPhotoURL: []\n    });\n  };\n\n  const resetUserEditData = (): void =>\n    setEditUserData({\n      bio,\n      name,\n      website,\n      photoURL,\n      location,\n      coverPhotoURL\n    });\n\n  const handleChange =\n    (key: EditableData) =>\n    ({\n      target: { value }\n    }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>\n      setEditUserData({ ...editUserData, [key]: value });\n\n  const handleKeyboardShortcut = ({\n    key,\n    target,\n    ctrlKey\n  }: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>): void => {\n    if (ctrlKey && key === 'Enter' && !inputNameError) {\n      target.blur();\n      void updateData();\n    }\n  };\n\n  const inputFields: Readonly<RequiredInputFieldProps[]> = [\n    {\n      label: 'Name',\n      inputId: 'name',\n      inputValue: editUserData.name,\n      inputLimit: 50,\n      errorMessage: inputNameError\n    },\n    {\n      label: 'Bio',\n      inputId: 'bio',\n      inputValue: editUserData.bio,\n      inputLimit: 160,\n      useTextArea: true\n    },\n    {\n      label: 'Location',\n      inputId: 'location',\n      inputValue: editUserData.location,\n      inputLimit: 30\n    },\n    {\n      label: 'Website',\n      inputId: 'website',\n      inputValue: editUserData.website,\n      inputLimit: 100\n    }\n  ];\n\n  return (\n    <form className={cn(hide && 'hidden md:block')}>\n      <Modal\n        modalClassName='relative bg-main-background rounded-2xl max-w-xl w-full h-[672px] overflow-hidden'\n        open={open}\n        closeModal={closeModal}\n      >\n        <EditProfileModal\n          name={name}\n          loading={loading}\n          photoURL={editUserData.photoURL}\n          coverPhotoURL={editUserData.coverPhotoURL}\n          inputNameError={inputNameError}\n          editImage={editImage}\n          closeModal={closeModal}\n          updateData={updateData}\n          removeCoverImage={removeCoverImage}\n          resetUserEditData={resetUserEditData}\n        >\n          {inputFields.map((inputData) => (\n            <InputField\n              {...inputData}\n              handleChange={handleChange(inputData.inputId)}\n              handleKeyboardShortcut={handleKeyboardShortcut}\n              key={inputData.inputId}\n            />\n          ))}\n        </EditProfileModal>\n      </Modal>\n      <Button\n        className='dark-bg-tab self-start border border-light-line-reply px-4 py-1.5 font-bold\n                   hover:bg-light-primary/10 active:bg-light-primary/20 dark:border-light-secondary\n                   dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20'\n        onClick={openModal}\n      >\n        Edit profile\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-fid.tsx",
    "content": "type UserFollowingProps = {\n  userId: string;\n};\n\nexport function UserFid({\n  userId: userTargetId\n}: UserFollowingProps): JSX.Element | null {\n  return (\n    <p\n      className='rounded bg-main-search-background px-1 text-xs'\n      title={`FID ${userTargetId}`}\n    >\n      # {parseInt(userTargetId).toLocaleString()}\n    </p>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-follow-stats.tsx",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport { useState, useEffect, useMemo } from 'react';\nimport Link from 'next/link';\nimport { useRouter } from 'next/router';\nimport { NumberStats } from '@components/tweet/number-stats';\nimport type { User, UserFull } from '@lib/types/user';\n\ntype UserFollowStatsProps = Pick<UserFull, 'following' | 'followers'>;\ntype Stats = [string, string, number, number];\n\nexport function UserFollowStats({\n  following,\n  followers\n}: UserFollowStatsProps): JSX.Element {\n  const totalFollowing = following.length;\n  const totalFollowers = followers.length;\n\n  const [{ currentFollowers, currentFollowing }, setCurrentStats] = useState({\n    currentFollowing: totalFollowing,\n    currentFollowers: totalFollowers\n  });\n\n  useEffect(() => {\n    setCurrentStats({\n      currentFollowing: totalFollowing,\n      currentFollowers: totalFollowers\n    });\n  }, [totalFollowing, totalFollowers]);\n\n  const followingMove = useMemo(\n    () => (totalFollowing > currentFollowing ? -25 : 25),\n    [totalFollowing]\n  );\n\n  const followersMove = useMemo(\n    () => (totalFollowers > currentFollowers ? -25 : 25),\n    [totalFollowers]\n  );\n\n  const {\n    query: { id }\n  } = useRouter();\n\n  const userPath = `/user/${id as string}`;\n\n  const allStats: Readonly<Stats[]> = [\n    ['Following', `${userPath}/following`, followingMove, currentFollowing],\n    ['Follower', `${userPath}/followers`, followersMove, currentFollowers]\n  ];\n\n  return (\n    <div\n      className='flex gap-4 text-light-secondary dark:text-dark-secondary\n                 [&>a>div]:font-bold [&>a>div]:text-light-primary \n                 dark:[&>a>div]:text-dark-primary'\n    >\n      {allStats.map(([title, link, move, stats], index) => (\n        <Link\n          href={link}\n          key={title}\n          className='hover-animation mb-[3px] mt-0.5 flex h-4 items-center gap-1 border-b \n                       border-b-transparent outline-none hover:border-b-light-primary \n                       focus-visible:border-b-light-primary dark:hover:border-b-dark-primary\n                       dark:focus-visible:border-b-dark-primary'\n        >\n          <NumberStats move={move} stats={stats} alwaysShowStats />\n          <p>{index === 1 && stats > 1 ? `${title}s` : title}</p>\n        </Link>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-follow.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { UserCards } from '@components/user/user-cards';\nimport { useUser } from '@lib/context/user-context';\nimport type { User } from '@lib/types/user';\nimport { useInfiniteScrollUsers } from '../../lib/hooks/useInfiniteScrollUsers';\n\ntype UserFollowProps = {\n  type: 'following' | 'followers';\n};\n\nexport function UserFollow({ type }: UserFollowProps): JSX.Element {\n  const { user } = useUser();\n  const { name, username, id: userId } = user as User;\n\n  const { data, loading, LoadMore } = useInfiniteScrollUsers(\n    (pageParam) => {\n      return `/api/user/${userId}/links?type=${type}&limit=10${\n        pageParam ? `&cursor=${pageParam}` : ''\n      }`;\n    },\n    {\n      queryKey: [userId, type]\n    }\n  );\n\n  return (\n    <>\n      <SEO\n        title={`People ${\n          type === 'following' ? 'followed by' : 'following'\n        } ${name} (@${username}) / Opencast`}\n      />\n      <UserCards\n        follow\n        data={\n          (data?.pages\n            .map((page) => page?.users)\n            .flat()\n            .filter((user) => user !== undefined) as User[]) ?? []\n        }\n        type={type}\n        loading={loading}\n        LoadMore={LoadMore}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-following.tsx",
    "content": "import { useAuth } from '@lib/context/auth-context';\n\ntype UserFollowingProps = {\n  userTargetId: string;\n};\n\nexport function UserFollowing({\n  userTargetId\n}: UserFollowingProps): JSX.Element | null {\n  const { user } = useAuth();\n\n  const isFollowing =\n    user?.keyPair &&\n    user?.id !== userTargetId &&\n    user?.followers.includes(userTargetId);\n\n  if (!isFollowing) return null;\n\n  return (\n    <p className='rounded bg-main-search-background px-1 text-xs'>\n      Follows you\n    </p>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-header.tsx",
    "content": "import { useUser } from '@lib/context/user-context';\nimport { isPlural } from '@lib/utils';\nimport type { Variants } from 'framer-motion';\nimport { useRouter } from 'next/router';\nimport { formatNumber } from '../../lib/date';\nimport { UserName } from './user-name';\n\nexport const variants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1, transition: { duration: 0.4 } },\n  exit: { opacity: 0, transition: { duration: 0.2 } }\n};\n\nexport function UserHeader(): JSX.Element {\n  const {\n    pathname,\n    query: { id }\n  } = useRouter();\n\n  const { user, loading } = useUser();\n\n  const [totalTweets, totalPhotos] = [\n    user?.totalTweets ?? 0,\n    user?.totalPhotos\n  ];\n\n  const currentPage = pathname.split('/').pop() ?? '';\n\n  const isInTweetPage = ['[id]', 'with_replies'].includes(currentPage);\n  const isInFollowPage = ['following', 'followers'].includes(currentPage);\n\n  return (\n    // <AnimatePresence mode='popLayout'>\n    <div>\n      {loading ? (\n        <div\n          className='-mb-1 inner:animate-pulse inner:rounded-lg \n                     inner:bg-light-secondary dark:inner:bg-dark-secondary'\n          {...variants}\n          key='loading'\n        >\n          <div className='-mt-1 mb-1 h-5 w-24' />\n          <div className='h-4 w-12' />\n        </div>\n      ) : !user ? (\n        <h2 className='text-xl font-bold' {...variants} key='not-found'>\n          {isInFollowPage ? `@${id as string}` : 'User'}\n        </h2>\n      ) : (\n        <div className='-mb-1 truncate' {...variants} key='found'>\n          <UserName\n            tag='h2'\n            name={user.name}\n            className='-mt-1 text-xl'\n            iconClassName='w-6 h-6'\n            verified={user.verified}\n          />\n          <p className='text-xs text-light-secondary dark:text-dark-secondary'>\n            {isInFollowPage\n              ? `@${user.username}`\n              : isInTweetPage\n              ? totalTweets\n                ? `${formatNumber(totalTweets)} ${`Cast${isPlural(\n                    totalTweets\n                  )}`}`\n                : 'No Casts'\n              : currentPage === 'media'\n              ? totalPhotos\n                ? `${formatNumber(totalPhotos)} Photo${isPlural(\n                    totalPhotos\n                  )} & GIF${isPlural(totalPhotos)}`\n                : 'No Photo & GIF'\n              : ''}\n          </p>\n        </div>\n      )}\n      {/* </AnimatePresence> */}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-home-avatar.tsx",
    "content": "import { useModal } from '@lib/hooks/useModal';\nimport { Button } from '@components/ui/button';\nimport { NextImage } from '@components/ui/next-image';\nimport { Modal } from '@components/modal/modal';\nimport { ImageModal } from '@components/modal/image-modal';\nimport type { ImageData } from '@lib/types/file';\n\ntype UserHomeAvatarProps = {\n  profileData?: ImageData | null;\n};\n\nexport function UserHomeAvatar({\n  profileData\n}: UserHomeAvatarProps): JSX.Element {\n  const { open, openModal, closeModal } = useModal();\n\n  return (\n    <div>\n      <Modal open={open} closeModal={closeModal}>\n        <ImageModal\n          imageData={\n            { src: profileData?.src, alt: profileData?.alt } as ImageData\n          }\n          previewCount={1}\n        />\n      </Modal>\n      <Button\n        className='accent-tab aspect-square w-24 overflow-hidden p-0 \n                   disabled:cursor-auto disabled:opacity-100 xs:w-32 sm:w-36\n                   [&:hover>figure>span]:brightness-75'\n        onClick={openModal}\n        disabled={!profileData}\n      >\n        {profileData ? (\n          <NextImage\n            useSkeleton\n            className='hover-animation relative h-full w-full bg-main-background\n                       inner:!m-1 inner:rounded-full inner:transition inner:duration-200'\n            imgClassName='rounded-full'\n            src={profileData.src}\n            alt={profileData.alt}\n            layout='fill'\n            key={profileData.src}\n          />\n        ) : (\n          <div className='h-full rounded-full bg-main-background p-1'>\n            <div className='h-full rounded-full bg-main-sidebar-background' />\n          </div>\n        )}\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-home-cover.tsx",
    "content": "import { useModal } from '@lib/hooks/useModal';\nimport { Button } from '@components/ui/button';\nimport { NextImage } from '@components/ui/next-image';\nimport { Modal } from '@components/modal/modal';\nimport { ImageModal } from '@components/modal/image-modal';\nimport type { ImageData } from '@lib/types/file';\n\ntype UserHomeCoverProps = {\n  coverData?: ImageData | null;\n};\n\nexport function UserHomeCover({ coverData }: UserHomeCoverProps): JSX.Element {\n  const { open, openModal, closeModal } = useModal();\n\n  return (\n    <div className='mt-0.5 h-36 xs:h-48 sm:h-52'>\n      <Modal open={open} closeModal={closeModal}>\n        <ImageModal imageData={coverData as ImageData} previewCount={1} />\n      </Modal>\n      {coverData ? (\n        <Button\n          className='accent-tab relative h-full w-full rounded-none p-0 transition hover:brightness-75'\n          onClick={openModal}\n        >\n          <NextImage\n            useSkeleton\n            layout='fill'\n            imgClassName='object-cover'\n            src={coverData.src}\n            alt={coverData.alt}\n            key={coverData.src}\n          />\n        </Button>\n      ) : (\n        <div className='h-full bg-light-line-reply dark:bg-dark-line-reply' />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-known-followers.tsx",
    "content": "import useSWR from 'swr';\nimport { useAuth } from '../../lib/context/auth-context';\nimport { fetchJSON } from '../../lib/fetch';\nimport { KnownFollowersResponse, User } from '../../lib/types/user';\nimport { UserAvatar } from './user-avatar';\n\nexport function UserKnownFollowersLazy({\n  userId,\n  enabled: shouldFetch = true\n}: {\n  enabled?: boolean;\n  userId: string;\n}) {\n  const { user: currentUser } = useAuth();\n\n  const { data: knownFollowersResponse, isValidating: loadingKnownFollowers } =\n    useSWR(\n      shouldFetch && currentUser\n        ? `/api/user/${userId}/known-followers?context_id=${currentUser.id}`\n        : null,\n      async (url) => (await fetchJSON<KnownFollowersResponse>(url)).result,\n      { revalidateOnFocus: false }\n    );\n\n  return !knownFollowersResponse ? (\n    loadingKnownFollowers ? (\n      // <UserKnownFollowersSkeleton />\n      <></>\n    ) : (\n      <></>\n    )\n  ) : (\n    <UserKnownFollowers\n      resolvedUsers={knownFollowersResponse.resolvedUsers}\n      knownFollowerCount={knownFollowersResponse.knownFollowerCount}\n    />\n  );\n}\n\nexport function UserKnownFollowers({\n  resolvedUsers,\n  knownFollowerCount\n}: {\n  resolvedUsers: User[];\n  knownFollowerCount: number;\n}) {\n  const otherKnownFollowerCount = knownFollowerCount - resolvedUsers.length;\n  return (\n    knownFollowerCount > 0 && (\n      <div className='flex text-sm text-light-secondary dark:text-dark-secondary'>\n        <div className='ml-2 mt-1 flex'>\n          {resolvedUsers.slice(0, 3).map((user) => (\n            <span className='-ml-2'>\n              <UserAvatar\n                src={user.photoURL}\n                alt={user.name}\n                size={18}\n                username={user.username}\n              />\n            </span>\n          ))}\n        </div>\n        <div className='ml-2'>\n          <span>Followed by </span>\n          {resolvedUsers\n            .slice(0, 2)\n            .map((user) => user.name)\n            .join(', ')}\n          <span>\n            {otherKnownFollowerCount > 0 &&\n              `, and ${otherKnownFollowerCount} other${\n                otherKnownFollowerCount > 1 ? 's' : ''\n              } you follow`}\n          </span>\n        </div>\n      </div>\n    )\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-name.tsx",
    "content": "import cn from 'clsx';\nimport Link from 'next/link';\nimport { HeroIcon } from '@components/ui/hero-icon';\n\ntype UserNameProps = {\n  tag?: keyof JSX.IntrinsicElements;\n  name: string;\n  verified: boolean;\n  username?: string;\n  className?: string;\n  iconClassName?: string;\n};\n\nexport function UserName({\n  tag,\n  name,\n  verified,\n  username,\n  className,\n  iconClassName\n}: UserNameProps): JSX.Element {\n  const CustomTag = tag ? tag : 'p';\n\n  return (\n    <Link href={username ? `/user/${username}` : '#'}>\n      <span\n        className={cn(\n          'flex items-center gap-1 truncate font-bold',\n          username ? 'custom-underline' : 'pointer-events-none',\n          className\n        )}\n        tabIndex={username ? 0 : -1}\n      >\n        <CustomTag className='override-nav inline truncate'>{name}</CustomTag>\n        {verified && (\n          <i>\n            <HeroIcon\n              className={cn('fill-accent-blue', iconClassName ?? 'h-5 w-5')}\n              iconName='CheckBadgeIcon'\n              solid\n            />\n          </i>\n        )}\n      </span>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-nav.tsx",
    "content": "import { motion } from 'framer-motion';\nimport cn from 'clsx';\nimport { variants } from '@components/user/user-header';\nimport { SegmentedNavLink } from '../ui/segmented-nav-link';\n\ntype UserNavProps = {\n  follow?: boolean;\n  userId: string;\n};\n\nconst allNavs = [\n  [\n    { name: 'Casts', path: '' },\n    { name: 'Casts & replies', path: '/with_replies' },\n    // { name: 'Media', path: 'media' },\n    { name: 'Likes', path: '/likes' }\n  ],\n  [\n    { name: 'Following', path: 'following' },\n    { name: 'Followers', path: 'followers' }\n  ]\n] as const;\n\nexport function UserNav({ follow, userId }: UserNavProps): JSX.Element {\n  const userNav = allNavs[+!!follow];\n\n  const userPath = `/user/${userId}`;\n\n  return (\n    <motion.nav\n      className={cn(\n        `hover-animation flex justify-between overflow-y-auto\n         border-b border-light-border dark:border-dark-border`,\n        follow && 'mb-0.5 mt-1'\n      )}\n      {...variants}\n      exit={undefined}\n    >\n      {userNav.map(({ name, path }) => (\n        <SegmentedNavLink name={name} path={`${userPath}${path}`} key={name} />\n      ))}\n    </motion.nav>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-share.tsx",
    "content": "import cn from 'clsx';\nimport { Popover } from '@headlessui/react';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { toast } from 'react-hot-toast';\nimport { preventBubbling } from '@lib/utils';\nimport { siteURL } from '@lib/env';\nimport { Button } from '@components/ui/button';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { variants } from '@components/tweet/tweet-actions';\n\ntype UserShareProps = {\n  username: string;\n};\n\nexport function UserShare({ username }: UserShareProps): JSX.Element {\n  const handleCopy = (closeMenu: () => void) => async (): Promise<void> => {\n    closeMenu();\n    await navigator.clipboard.writeText(`${siteURL}/user/${username}`);\n    toast.success('Copied to clipboard');\n  };\n\n  return (\n    <Popover className='relative'>\n      {({ open, close }): JSX.Element => (\n        <>\n          <Popover.Button\n            as={Button}\n            className={cn(\n              `dark-bg-tab group relative border border-light-line-reply p-2\n               hover:bg-light-primary/10 active:bg-light-primary/20 dark:border-light-secondary\n               dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20`,\n              open && 'bg-light-primary/10 dark:bg-dark-primary/10'\n            )}\n          >\n            <HeroIcon className='h-5 w-5' iconName='EllipsisHorizontalIcon' />\n            {!open && <ToolTip tip='More' />}\n          </Popover.Button>\n          <AnimatePresence>\n            {open && (\n              <Popover.Panel\n                className='menu-container group absolute right-0 top-11 whitespace-nowrap\n                           text-light-primary dark:text-dark-primary'\n                as={motion.div}\n                {...variants}\n                static\n              >\n                <Popover.Button\n                  className='flex w-full gap-3 rounded-md rounded-b-none p-4 hover:bg-main-sidebar-background'\n                  as={Button}\n                  onClick={preventBubbling(handleCopy(close))}\n                >\n                  <HeroIcon iconName='LinkIcon' />\n                  Copy link to Profile\n                </Popover.Button>\n              </Popover.Panel>\n            )}\n          </AnimatePresence>\n        </>\n      )}\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-tooltip.tsx",
    "content": "import { FollowButton } from '@components/ui/follow-button';\nimport { useWindow } from '@lib/context/window-context';\nimport type { User, UserFullResponse } from '@lib/types/user';\nimport cn from 'clsx';\nimport Link from 'next/link';\nimport { useState, type ReactNode } from 'react';\nimport useSWR from 'swr';\nimport { useAuth } from '../../lib/context/auth-context';\nimport { formatNumber } from '../../lib/date';\nimport { fetchJSON } from '../../lib/fetch';\nimport { TweetText } from '../tweet/tweet-text';\nimport { TopicView } from '../tweet/tweet-topic';\nimport { Loading } from '../ui/loading';\nimport { UserAvatar } from './user-avatar';\nimport { UserFid } from './user-fid';\nimport { UserFollowing } from './user-following';\nimport { UserKnownFollowersLazy } from './user-known-followers';\nimport { UserName } from './user-name';\nimport { UserUsername } from './user-username';\n\ntype UserTooltipProps = Pick<\n  User,\n  'id' | 'bio' | 'name' | 'verified' | 'username' | 'photoURL'\n> & {\n  modal?: boolean;\n  avatar?: boolean;\n  children: ReactNode;\n};\n\ntype Stats = [string, string, number];\n\nexport function UserTooltip({\n  id,\n  bio,\n  name,\n  modal,\n  avatar,\n  verified,\n  children,\n  photoURL,\n  username\n}: UserTooltipProps): JSX.Element {\n  const { isMobile } = useWindow();\n  const { user: currentUser } = useAuth();\n\n  const [shouldFetch, setShouldFetch] = useState(false);\n  let hoverTimer: NodeJS.Timeout | null = null;\n\n  const handleMouseEnter = () => {\n    hoverTimer = setTimeout(() => {\n      setShouldFetch(true);\n    }, 500);\n  };\n\n  const handleMouseLeave = () => {\n    hoverTimer && clearTimeout(hoverTimer);\n    // setShouldFetch(false); // You can choose to keep it true if you want to keep the data\n  };\n\n  const { data: user, isValidating } = useSWR(\n    shouldFetch ? `/api/user/${id}` : null,\n    async (url) => (await fetchJSON<UserFullResponse>(url)).result,\n    { revalidateOnFocus: false }\n  );\n\n  const { following, followers, interests } = user || {};\n\n  if (isMobile || modal) return <>{children}</>;\n\n  const userLink = `/user/${username}`;\n\n  const allStats: Readonly<Stats[]> = [\n    ['following', 'Following', following?.length || 0],\n    ['followers', 'Followers', followers?.length || 0]\n  ];\n\n  return (\n    <div\n      className={cn(\n        'z-100 group relative cursor-pointer self-start text-light-primary dark:text-dark-primary',\n        avatar ? '[&>div]:translate-y-2' : 'grid [&>div]:translate-y-7'\n      )}\n    >\n      <span\n        className='override-nav inline'\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n      >\n        {children}\n      </span>\n      <div\n        className='menu-container invisible absolute left-1/2 w-72 -translate-x-1/2 rounded-2xl \n                   opacity-0 [transition:visibility_0ms_ease_400ms,opacity_200ms_ease_200ms] group-hover:visible \n                   group-hover:opacity-100 group-hover:delay-500'\n      >\n        {user ? (\n          <div className='flex flex-col gap-3 p-4'>\n            <div className='flex flex-col gap-2'>\n              <div className='-mx-4 -mt-4'>\n                <div className='h-16 rounded-t-2xl bg-light-line-reply dark:bg-dark-line-reply' />\n              </div>\n              <div className='flex justify-between'>\n                <div className='mb-10'>\n                  <UserAvatar\n                    className='absolute -translate-y-1/2 bg-main-background p-1 \n                             hover:brightness-100 [&:hover>figure>span]:brightness-75\n                             [&>figure>span]:[transition:200ms]'\n                    src={photoURL}\n                    alt={name}\n                    size={64}\n                    username={username}\n                  />\n                </div>\n                <FollowButton userTargetId={id} userTargetUsername={username} />\n              </div>\n              <div>\n                <UserName\n                  className='-mb-1 text-lg'\n                  name={name}\n                  username={username}\n                  verified={verified}\n                />\n                <div className='flex flex-wrap items-center gap-1 text-light-secondary dark:text-dark-secondary'>\n                  <UserUsername username={username} />\n                  <UserFid userId={id} />\n                  <UserFollowing userTargetId={id} />\n                </div>\n              </div>\n            </div>\n            {bio && <TweetText text={bio} mentions={[]} images={[]} />}\n            {interests && (\n              <div className='flex flex-wrap'>\n                {interests.map((topic) => (\n                  <Link href={`/topic?url=${topic.url}`} key={topic.url}>\n                    <span className='pr-2 text-light-secondary hover:underline dark:text-dark-secondary'>\n                      <TopicView topic={topic} />\n                    </span>\n                  </Link>\n                ))}\n              </div>\n            )}\n            <div className='text-secondary flex gap-4'>\n              {allStats.map(([id, label, stat]) => (\n                <Link\n                  href={`${userLink}/${id}`}\n                  key={id}\n                  className='hover-animation flex h-4 items-center gap-1 border-b border-b-transparent \n                             outline-none hover:border-b-light-primary focus-visible:border-b-light-primary\n                             dark:hover:border-b-dark-primary dark:focus-visible:border-b-dark-primary'\n                >\n                  <p className='font-bold text-light-primary dark:text-dark-primary'>\n                    {formatNumber(stat)}\n                  </p>\n                  <p className='text-light-secondary dark:text-dark-secondary'>\n                    {label}\n                  </p>\n                </Link>\n              ))}\n            </div>\n            {currentUser?.keyPair && currentUser?.id !== id && (\n              <UserKnownFollowersLazy userId={id} enabled={shouldFetch} />\n            )}\n          </div>\n        ) : isValidating ? (\n          <Loading className='p-4' />\n        ) : (\n          <div>Could not load user</div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/user/user-username.tsx",
    "content": "import Link from 'next/link';\nimport cn from 'clsx';\n\ntype UserUsernameProps = {\n  username: string;\n  className?: string;\n  disableLink?: boolean;\n};\n\nexport function UserUsername({\n  username,\n  className,\n  disableLink\n}: UserUsernameProps): JSX.Element {\n  return (\n    <Link href={`/user/${username}`}>\n      <span\n        className={cn(\n          'override-nav inline truncate text-light-secondary dark:text-dark-secondary',\n          className,\n          disableLink && 'pointer-events-none'\n        )}\n        tabIndex={-1}\n      >\n        @{username}\n      </span>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/view/view-parent-tweet.tsx",
    "content": "import { Tweet } from '@components/tweet/tweet';\nimport { RefObject, useEffect, useMemo } from 'react';\nimport useSWR from 'swr';\nimport { fetchJSON } from '../../lib/fetch';\nimport { TweetResponse } from '../../lib/types/tweet';\n\ntype ViewParentTweetProps = {\n  parentId: string;\n  viewTweetRef: RefObject<HTMLElement>;\n};\n\nexport function ViewParentTweet({\n  parentId,\n  viewTweetRef\n}: ViewParentTweetProps): JSX.Element | null {\n  const { data, isValidating: loading } = useSWR(\n    `/api/tweet/${parentId}`,\n    async (url) => (await fetchJSON<TweetResponse>(url)).result\n  );\n\n  const mentions = useMemo(() => {\n    // Look up mentions in users object\n    const resolvedMentions = data?.mentions.map((mention) => ({\n      ...mention,\n      username: data.users[mention.userId]?.username\n    }));\n    return resolvedMentions;\n  }, [data]);\n\n  useEffect(() => {\n    if (!loading) viewTweetRef.current?.scrollIntoView();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [data?.id]);\n\n  if (!data && !loading)\n    return (\n      <div className='px-4 pb-2 pt-3'>\n        <p\n          className='rounded-2xl bg-main-sidebar-background px-1 py-3 pl-4 \n                     text-light-secondary dark:text-dark-secondary'\n        >\n          This Cast was deleted by the Cast author.{' '}\n          <a\n            className='custom-underline text-main-accent'\n            href='https://help.twitter.com/rules-and-policies/notices-on-twitter'\n            target='_blank'\n            rel='noreferrer'\n          >\n            Learn more\n          </a>\n        </p>\n      </div>\n    );\n\n  return data ? (\n    <>\n      {data.parent && (\n        <ViewParentTweet\n          parentId={data.parent.id}\n          viewTweetRef={viewTweetRef}\n        />\n      )}\n      <Tweet\n        parentTweet\n        {...data}\n        mentions={mentions || []}\n        user={data.users[data.createdBy]}\n      />\n    </>\n  ) : (\n    <></>\n  );\n}\n"
  },
  {
    "path": "src/components/view/view-tweet-stats.tsx",
    "content": "import { Modal } from '@components/modal/modal';\nimport { TweetStatsModal } from '@components/modal/tweet-stats-modal';\nimport { NumberStats } from '@components/tweet/number-stats';\nimport { UserCards } from '@components/user/user-cards';\nimport { ReactionType } from '@farcaster/hub-web';\nimport { useModal } from '@lib/hooks/useModal';\nimport type { Tweet } from '@lib/types/tweet';\nimport cn from 'clsx';\nimport { useState } from 'react';\nimport { useInfiniteScrollUsers } from '../../lib/hooks/useInfiniteScrollUsers';\nimport { User } from '../../lib/types/user';\n\ntype viewTweetStats = Pick<Tweet, 'userRetweets' | 'userLikes'> & {\n  likeMove: number;\n  tweetMove: number;\n  replyMove: number;\n  currentLikes: number;\n  currentTweets: number;\n  currentReplies: number;\n  isStatsVisible: boolean;\n  tweetId: string;\n};\n\nexport type StatsType = 'retweets' | 'likes';\n\ntype Stats = [string, StatsType | null, number, number];\n\nexport function ViewTweetStats({\n  likeMove,\n  userLikes,\n  tweetMove,\n  replyMove,\n  userRetweets,\n  currentLikes,\n  currentTweets,\n  currentReplies,\n  isStatsVisible,\n  tweetId\n}: viewTweetStats): JSX.Element {\n  const [statsType, setStatsType] = useState<StatsType | null>(null);\n\n  const { open, openModal, closeModal } = useModal();\n\n  const { data, loading, LoadMore } = useInfiniteScrollUsers(\n    (pageParam) => {\n      return `/api/tweet/${tweetId}/engagers?type=${\n        statsType === 'likes' ? ReactionType.LIKE : ReactionType.RECAST\n      }&limit=10${pageParam ? `&cursor=${pageParam}` : ''}`;\n    },\n    {\n      queryKey: [statsType, tweetId],\n      enabled: open\n    }\n  );\n\n  const handleOpen = (type: StatsType) => (): void => {\n    setStatsType(type);\n    openModal();\n  };\n\n  const handleClose = (): void => {\n    setStatsType(null);\n    closeModal();\n  };\n\n  const allStats: Readonly<Stats[]> = [\n    ['Reply', null, replyMove, currentReplies],\n    ['Recast', 'retweets', tweetMove, currentTweets],\n    ['Like', 'likes', likeMove, currentLikes]\n  ];\n\n  return (\n    <>\n      <Modal\n        modalClassName='relative bg-main-background rounded-2xl max-w-xl w-full \n                        h-[672px] overflow-hidden rounded-2xl'\n        open={open}\n        closeModal={handleClose}\n      >\n        <TweetStatsModal statsType={statsType} handleClose={handleClose}>\n          {/* <div className='mt-10'></div> */}\n          <UserCards\n            follow\n            type={statsType as StatsType}\n            data={\n              (data?.pages\n                .map((page) => page?.users)\n                .flat()\n                .filter((user) => user !== undefined) as User[]) ?? []\n            }\n            loading={loading}\n            LoadMore={LoadMore}\n          />\n        </TweetStatsModal>\n      </Modal>\n      {isStatsVisible && (\n        <div\n          className='flex gap-4 px-1 py-4 text-light-secondary dark:text-dark-secondary\n                     [&>button>div]:font-bold [&>button>div]:text-light-primary \n                     dark:[&>button>div]:text-dark-primary'\n        >\n          {allStats.map(\n            ([title, type, move, stats], index) =>\n              !!stats && (\n                <button\n                  className={cn(\n                    `hover-animation mb-[3px] mt-0.5 flex h-4 items-center gap-1 border-b \n                     border-b-transparent outline-none hover:border-b-light-primary \n                     focus-visible:border-b-light-primary dark:hover:border-b-dark-primary\n                     dark:focus-visible:border-b-dark-primary`,\n                    index === 0 && 'cursor-not-allowed'\n                  )}\n                  key={title}\n                  onClick={type ? handleOpen(type) : undefined}\n                >\n                  <NumberStats move={move} stats={stats} />\n                  <p>{`${\n                    stats === 1\n                      ? title\n                      : stats > 1 && index === 0\n                      ? `${title.slice(0, -1)}ies`\n                      : `${title}s`\n                  }`}</p>\n                </button>\n              )\n          )}\n        </div>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/view/view-tweet.tsx",
    "content": "import { ImagePreview } from '@components/input/image-preview';\nimport { Input } from '@components/input/input';\nimport { Modal } from '@components/modal/modal';\nimport { TweetReplyModal } from '@components/modal/tweet-reply-modal';\nimport { TweetActions } from '@components/tweet/tweet-actions';\nimport { TweetDate } from '@components/tweet/tweet-date';\nimport { TweetStats } from '@components/tweet/tweet-stats';\nimport { UserAvatar } from '@components/user/user-avatar';\nimport { UserName } from '@components/user/user-name';\nimport { UserTooltip } from '@components/user/user-tooltip';\nimport { UserUsername } from '@components/user/user-username';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport type { Tweet } from '@lib/types/tweet';\nimport type { User, UsersMapType } from '@lib/types/user';\nimport cn from 'clsx';\nimport Link from 'next/link';\nimport { RefObject } from 'react';\nimport { TweetEmbeds } from '../tweet/tweet-embed';\nimport { TweetText } from '../tweet/tweet-text';\nimport { TweetTopic } from '../tweet/tweet-topic';\nimport { Tweet as TweetView } from '@components/tweet/tweet';\n\ntype ViewTweetProps = Tweet & {\n  user: User;\n  usersMap?: UsersMapType<User>;\n  viewTweetRef?: RefObject<HTMLElement>;\n};\n\nexport function ViewTweet(tweet: ViewTweetProps): JSX.Element {\n  const {\n    id: tweetId,\n    text,\n    images,\n    parent,\n    userLikes,\n    createdBy,\n    createdAt,\n    userRetweets,\n    userReplies,\n    viewTweetRef,\n    mentions,\n    client,\n    topic,\n    embeds,\n    user: tweetUserData\n  } = tweet;\n\n  const { id: ownerId, name, username, verified, photoURL } = tweetUserData;\n\n  const { user } = useAuth();\n\n  const { open, openModal, closeModal } = useModal();\n\n  const tweetLink = `/tweet/${tweetId}`;\n\n  const userId = user?.id as string;\n\n  const isOwner = userId === createdBy;\n\n  const reply = !!parent;\n\n  const { id: parentId, username: parentUsername = username } = parent ?? {};\n\n  return (\n    <article\n      className={cn(\n        `accent-tab h- relative flex cursor-default flex-col gap-3 border-b\n         border-light-border px-4 py-3 outline-none dark:border-dark-border`,\n        reply && 'scroll-m-[3.25rem] pt-0'\n      )}\n      // {...variants}\n      // animate={{ ...variants.animate, transition: { duration: 0.2 } }}\n      // exit={undefined}\n      ref={viewTweetRef}\n    >\n      <Modal\n        className='flex items-start justify-center'\n        modalClassName='bg-main-background rounded-2xl max-w-xl w-full mt-8 overflow-hidden'\n        open={open}\n        closeModal={closeModal}\n      >\n        <TweetReplyModal tweet={tweet} closeModal={closeModal} />\n      </Modal>\n      <div className='flex flex-col gap-2'>\n        {reply && (\n          <div className='flex w-12 items-center justify-center'>\n            <i className='hover-animation h-2 w-0.5 bg-light-line-reply dark:bg-dark-line-reply' />\n          </div>\n        )}\n        <div className='grid grid-cols-[auto,1fr] gap-3'>\n          <UserTooltip avatar {...tweetUserData}>\n            <UserAvatar src={photoURL} alt={name} username={username} />\n          </UserTooltip>\n          <div className='flex min-w-0 justify-between'>\n            <div className='flex cursor-pointer flex-col truncate xs:overflow-visible xs:whitespace-normal '>\n              <UserTooltip {...tweetUserData}>\n                <UserName\n                  className='-mb-1'\n                  name={name}\n                  username={username}\n                  verified={verified}\n                />\n              </UserTooltip>\n              <UserTooltip {...tweetUserData}>\n                <UserUsername username={username} />\n              </UserTooltip>\n            </div>\n            <div className='px-4'>\n              <TweetActions\n                viewTweet\n                isOwner={isOwner}\n                ownerId={ownerId}\n                tweetId={tweetId}\n                parentId={parentId}\n                username={username}\n                hasImages={!!images}\n                createdBy={createdBy}\n                topic={topic || undefined}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n      {reply && (\n        <p className='text-light-secondary dark:text-dark-secondary'>\n          Replying to{' '}\n          <Link\n            href={`/user/${parentUsername}`}\n            className='custom-underline text-main-accent'\n          >\n            @{parentUsername}\n          </Link>\n        </p>\n      )}\n      <div>\n        <TweetText text={text || ''} images={images} mentions={mentions} />\n        {images && (\n          <ImagePreview\n            viewTweet\n            imagesPreview={images}\n            previewCount={images.length}\n          />\n        )}\n        {embeds && embeds.length > 0 && <TweetEmbeds embeds={embeds} tweetAuthorId={createdBy} tweetId={tweetId} />}\n\n        {\n          tweet.usersMap && tweet.quoteTweets?.map((quoteTweet) => (\n            <TweetView\n              key={quoteTweet.id}\n              {...quoteTweet}\n              user={tweet.usersMap![quoteTweet.createdBy]}\n              parentTweet={false}\n              quoted\n            />\n          ))\n        }\n        {topic && (\n          <span className='mt-2 inline-block'>\n            <TweetTopic topic={topic} />\n          </span>\n        )}\n        <div\n          className='inner:hover-animation inner:border-b inner:border-light-border\n                     dark:inner:border-dark-border'\n        >\n          <div className='flex '>\n            <span className='flex flex-wrap items-center gap-1 py-4 text-light-secondary dark:text-dark-secondary'>\n              <TweetDate\n                viewTweet\n                tweetLink={tweetLink}\n                createdAt={createdAt}\n              />\n              {client && (\n                <>\n                  <i className='px-1  '>·</i>{' '}\n                  <span className='inline'>via {client}</span>\n                </>\n              )}\n            </span>\n          </div>\n          <TweetStats\n            viewTweet\n            reply={reply}\n            userId={userId}\n            isOwner={isOwner}\n            tweetId={tweetId}\n            userLikes={userLikes}\n            userRetweets={userRetweets}\n            userReplies={userReplies}\n            openModal={openModal}\n            tweetAuthorId={ownerId}\n          />\n        </div>\n        {user?.keyPair && (\n          <Input\n            reply\n            parent={{ id: tweetId, username: username, userId: ownerId }}\n            parentUrl={topic?.url || undefined}\n          />\n        )}\n      </div>\n    </article>\n  );\n}\n"
  },
  {
    "path": "src/contracts/id-registry.ts",
    "content": "const ID_REGISTRY_ADDRESS =\n  '0x00000000fc6c5f01fc30151999387bb99a9f489b' as `0x${string}`\n\nconst ID_REGISTRY_ABI = [\n  {\n    inputs: [\n      { internalType: 'address', name: '_initialOwner', type: 'address' },\n    ],\n    stateMutability: 'nonpayable',\n    type: 'constructor',\n  },\n  { inputs: [], name: 'HasId', type: 'error' },\n  { inputs: [], name: 'HasNoId', type: 'error' },\n  {\n    inputs: [\n      { internalType: 'address', name: 'account', type: 'address' },\n      { internalType: 'uint256', name: 'currentNonce', type: 'uint256' },\n    ],\n    name: 'InvalidAccountNonce',\n    type: 'error',\n  },\n  { inputs: [], name: 'InvalidAddress', type: 'error' },\n  { inputs: [], name: 'InvalidShortString', type: 'error' },\n  { inputs: [], name: 'InvalidSignature', type: 'error' },\n  { inputs: [], name: 'OnlyTrustedCaller', type: 'error' },\n  { inputs: [], name: 'Registrable', type: 'error' },\n  { inputs: [], name: 'Seedable', type: 'error' },\n  { inputs: [], name: 'SignatureExpired', type: 'error' },\n  {\n    inputs: [{ internalType: 'string', name: 'str', type: 'string' }],\n    name: 'StringTooLong',\n    type: 'error',\n  },\n  { inputs: [], name: 'Unauthorized', type: 'error' },\n  {\n    anonymous: false,\n    inputs: [\n      { indexed: true, internalType: 'uint256', name: 'id', type: 'uint256' },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'recovery',\n        type: 'address',\n      },\n    ],\n    name: 'ChangeRecoveryAddress',\n    type: 'event',\n  },\n  { anonymous: false, inputs: [], name: 'DisableTrustedOnly', type: 'event' },\n  { anonymous: false, inputs: [], name: 'EIP712DomainChanged', type: 'event' },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address',\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address',\n      },\n    ],\n    name: 'OwnershipTransferStarted',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address',\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address',\n      },\n    ],\n    name: 'OwnershipTransferred',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'account',\n        type: 'address',\n      },\n    ],\n    name: 'Paused',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      { indexed: true, internalType: 'address', name: 'from', type: 'address' },\n      { indexed: true, internalType: 'address', name: 'to', type: 'address' },\n      { indexed: true, internalType: 'uint256', name: 'id', type: 'uint256' },\n    ],\n    name: 'Recover',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      { indexed: true, internalType: 'address', name: 'to', type: 'address' },\n      { indexed: true, internalType: 'uint256', name: 'id', type: 'uint256' },\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'recovery',\n        type: 'address',\n      },\n    ],\n    name: 'Register',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'oldCaller',\n        type: 'address',\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newCaller',\n        type: 'address',\n      },\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'owner',\n        type: 'address',\n      },\n    ],\n    name: 'SetTrustedCaller',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      { indexed: true, internalType: 'address', name: 'from', type: 'address' },\n      { indexed: true, internalType: 'address', name: 'to', type: 'address' },\n      { indexed: true, internalType: 'uint256', name: 'id', type: 'uint256' },\n    ],\n    name: 'Transfer',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'account',\n        type: 'address',\n      },\n    ],\n    name: 'Unpaused',\n    type: 'event',\n  },\n  {\n    inputs: [],\n    name: 'CHANGE_RECOVERY_ADDRESS_TYPEHASH',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'REGISTER_TYPEHASH',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'TRANSFER_TYPEHASH',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'VERSION',\n    outputs: [{ internalType: 'string', name: '', type: 'string' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'acceptOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'recovery', type: 'address' }],\n    name: 'changeRecoveryAddress',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'owner', type: 'address' },\n      { internalType: 'address', name: 'recovery', type: 'address' },\n      { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' },\n    ],\n    name: 'changeRecoveryAddressFor',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'disableTrustedOnly',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'domainSeparatorV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'eip712Domain',\n    outputs: [\n      { internalType: 'bytes1', name: 'fields', type: 'bytes1' },\n      { internalType: 'string', name: 'name', type: 'string' },\n      { internalType: 'string', name: 'version', type: 'string' },\n      { internalType: 'uint256', name: 'chainId', type: 'uint256' },\n      { internalType: 'address', name: 'verifyingContract', type: 'address' },\n      { internalType: 'bytes32', name: 'salt', type: 'bytes32' },\n      { internalType: 'uint256[]', name: 'extensions', type: 'uint256[]' },\n    ],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'bytes32', name: 'structHash', type: 'bytes32' }],\n    name: 'hashTypedDataV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'idCounter',\n    outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],\n    name: 'idOf',\n    outputs: [{ internalType: 'uint256', name: 'fid', type: 'uint256' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'name',\n    outputs: [{ internalType: 'string', name: '', type: 'string' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],\n    name: 'nonces',\n    outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'owner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'pause',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'paused',\n    outputs: [{ internalType: 'bool', name: '', type: 'bool' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'pendingOwner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'from', type: 'address' },\n      { internalType: 'address', name: 'to', type: 'address' },\n      { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' },\n    ],\n    name: 'recover',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'from', type: 'address' },\n      { internalType: 'address', name: 'to', type: 'address' },\n      { internalType: 'uint256', name: 'recoveryDeadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'recoverySig', type: 'bytes' },\n      { internalType: 'uint256', name: 'toDeadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'toSig', type: 'bytes' },\n    ],\n    name: 'recoverFor',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'uint256', name: 'fid', type: 'uint256' }],\n    name: 'recoveryOf',\n    outputs: [{ internalType: 'address', name: 'recovery', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'recovery', type: 'address' }],\n    name: 'register',\n    outputs: [{ internalType: 'uint256', name: 'fid', type: 'uint256' }],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'to', type: 'address' },\n      { internalType: 'address', name: 'recovery', type: 'address' },\n      { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' },\n    ],\n    name: 'registerFor',\n    outputs: [{ internalType: 'uint256', name: 'fid', type: 'uint256' }],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'renounceOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: '_trustedCaller', type: 'address' },\n    ],\n    name: 'setTrustedCaller',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'to', type: 'address' },\n      { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' },\n    ],\n    name: 'transfer',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'from', type: 'address' },\n      { internalType: 'address', name: 'to', type: 'address' },\n      { internalType: 'uint256', name: 'fromDeadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'fromSig', type: 'bytes' },\n      { internalType: 'uint256', name: 'toDeadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'toSig', type: 'bytes' },\n    ],\n    name: 'transferFor',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],\n    name: 'transferOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'trustedCaller',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'trustedOnly',\n    outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'to', type: 'address' },\n      { internalType: 'address', name: 'recovery', type: 'address' },\n    ],\n    name: 'trustedRegister',\n    outputs: [{ internalType: 'uint256', name: 'fid', type: 'uint256' }],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'unpause',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'custodyAddress', type: 'address' },\n      { internalType: 'uint256', name: 'fid', type: 'uint256' },\n      { internalType: 'bytes32', name: 'digest', type: 'bytes32' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' },\n    ],\n    name: 'verifyFidSignature',\n    outputs: [{ internalType: 'bool', name: 'isValid', type: 'bool' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n] as const\n\nexport const ID_REGISTRY = {\n  address: ID_REGISTRY_ADDRESS,\n  abi: ID_REGISTRY_ABI,\n}\n"
  },
  {
    "path": "src/contracts/index.ts",
    "content": "export * from './id-registry';\nexport * from './key-registry';\nexport * from './key-gateway';\n"
  },
  {
    "path": "src/contracts/key-gateway.ts",
    "content": "const KEY_GATEWAY_ADDRESS =\n  '0x00000000fC56947c7E7183f8Ca4B62398CaAdf0B' as `0x${string}`;\n\nconst KEY_GATEWAY_ABI = [\n  {\n    inputs: [\n      { internalType: 'address', name: '_keyRegistry', type: 'address' },\n      { internalType: 'address', name: '_initialOwner', type: 'address' }\n    ],\n    stateMutability: 'nonpayable',\n    type: 'constructor'\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'account', type: 'address' },\n      { internalType: 'uint256', name: 'currentNonce', type: 'uint256' }\n    ],\n    name: 'InvalidAccountNonce',\n    type: 'error'\n  },\n  { inputs: [], name: 'InvalidShortString', type: 'error' },\n  { inputs: [], name: 'InvalidSignature', type: 'error' },\n  { inputs: [], name: 'OnlyGuardian', type: 'error' },\n  { inputs: [], name: 'SignatureExpired', type: 'error' },\n  {\n    inputs: [{ internalType: 'string', name: 'str', type: 'string' }],\n    name: 'StringTooLong',\n    type: 'error'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'guardian',\n        type: 'address'\n      }\n    ],\n    name: 'Add',\n    type: 'event'\n  },\n  { anonymous: false, inputs: [], name: 'EIP712DomainChanged', type: 'event' },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address'\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address'\n      }\n    ],\n    name: 'OwnershipTransferStarted',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address'\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address'\n      }\n    ],\n    name: 'OwnershipTransferred',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'account',\n        type: 'address'\n      }\n    ],\n    name: 'Paused',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'guardian',\n        type: 'address'\n      }\n    ],\n    name: 'Remove',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'account',\n        type: 'address'\n      }\n    ],\n    name: 'Unpaused',\n    type: 'event'\n  },\n  {\n    inputs: [],\n    name: 'ADD_TYPEHASH',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'VERSION',\n    outputs: [{ internalType: 'string', name: '', type: 'string' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'acceptOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' },\n      { internalType: 'bytes', name: 'metadata', type: 'bytes' }\n    ],\n    name: 'add',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'fidOwner', type: 'address' },\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' },\n      { internalType: 'bytes', name: 'metadata', type: 'bytes' },\n      { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' }\n    ],\n    name: 'addFor',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'guardian', type: 'address' }],\n    name: 'addGuardian',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'domainSeparatorV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'eip712Domain',\n    outputs: [\n      { internalType: 'bytes1', name: 'fields', type: 'bytes1' },\n      { internalType: 'string', name: 'name', type: 'string' },\n      { internalType: 'string', name: 'version', type: 'string' },\n      { internalType: 'uint256', name: 'chainId', type: 'uint256' },\n      { internalType: 'address', name: 'verifyingContract', type: 'address' },\n      { internalType: 'bytes32', name: 'salt', type: 'bytes32' },\n      { internalType: 'uint256[]', name: 'extensions', type: 'uint256[]' }\n    ],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'guardian', type: 'address' }],\n    name: 'guardians',\n    outputs: [{ internalType: 'bool', name: 'isGuardian', type: 'bool' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'bytes32', name: 'structHash', type: 'bytes32' }],\n    name: 'hashTypedDataV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'keyRegistry',\n    outputs: [\n      { internalType: 'contract IKeyRegistry', name: '', type: 'address' }\n    ],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],\n    name: 'nonces',\n    outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'owner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'pause',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'paused',\n    outputs: [{ internalType: 'bool', name: '', type: 'bool' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'pendingOwner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'guardian', type: 'address' }],\n    name: 'removeGuardian',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'renounceOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],\n    name: 'transferOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'unpause',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'useNonce',\n    outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  }\n] as const;\n\nexport const KEY_GATEWAY = {\n  address: KEY_GATEWAY_ADDRESS,\n  abi: KEY_GATEWAY_ABI\n};\n"
  },
  {
    "path": "src/contracts/key-registry.ts",
    "content": "const KEY_REGISTRY_ADDRESS =\n  '0x00000000Fc1237824fb747aBDE0FF18990E59b7e' as `0x${string}`;\n\nconst KEY_REGISTRY_ABI = [\n  {\n    inputs: [\n      { internalType: 'address', name: '_idRegistry', type: 'address' },\n      { internalType: 'address', name: '_initialOwner', type: 'address' }\n    ],\n    stateMutability: 'nonpayable',\n    type: 'constructor'\n  },\n  { inputs: [], name: 'AlreadyMigrated', type: 'error' },\n  {\n    inputs: [\n      { internalType: 'address', name: 'account', type: 'address' },\n      { internalType: 'uint256', name: 'currentNonce', type: 'uint256' }\n    ],\n    name: 'InvalidAccountNonce',\n    type: 'error'\n  },\n  { inputs: [], name: 'InvalidAddress', type: 'error' },\n  { inputs: [], name: 'InvalidKeyType', type: 'error' },\n  { inputs: [], name: 'InvalidMetadata', type: 'error' },\n  { inputs: [], name: 'InvalidMetadataType', type: 'error' },\n  { inputs: [], name: 'InvalidShortString', type: 'error' },\n  { inputs: [], name: 'InvalidSignature', type: 'error' },\n  { inputs: [], name: 'InvalidState', type: 'error' },\n  { inputs: [], name: 'OnlyTrustedCaller', type: 'error' },\n  { inputs: [], name: 'Registrable', type: 'error' },\n  { inputs: [], name: 'Seedable', type: 'error' },\n  { inputs: [], name: 'SignatureExpired', type: 'error' },\n  {\n    inputs: [{ internalType: 'string', name: 'str', type: 'string' }],\n    name: 'StringTooLong',\n    type: 'error'\n  },\n  { inputs: [], name: 'Unauthorized', type: 'error' },\n  {\n    inputs: [\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' }\n    ],\n    name: 'ValidatorNotFound',\n    type: 'error'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      { indexed: true, internalType: 'uint256', name: 'fid', type: 'uint256' },\n      {\n        indexed: true,\n        internalType: 'uint32',\n        name: 'keyType',\n        type: 'uint32'\n      },\n      { indexed: true, internalType: 'bytes', name: 'key', type: 'bytes' },\n      {\n        indexed: false,\n        internalType: 'bytes',\n        name: 'keyBytes',\n        type: 'bytes'\n      },\n      {\n        indexed: false,\n        internalType: 'uint8',\n        name: 'metadataType',\n        type: 'uint8'\n      },\n      {\n        indexed: false,\n        internalType: 'bytes',\n        name: 'metadata',\n        type: 'bytes'\n      }\n    ],\n    name: 'Add',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      { indexed: true, internalType: 'uint256', name: 'fid', type: 'uint256' },\n      { indexed: true, internalType: 'bytes', name: 'key', type: 'bytes' },\n      {\n        indexed: false,\n        internalType: 'bytes',\n        name: 'keyBytes',\n        type: 'bytes'\n      }\n    ],\n    name: 'AdminReset',\n    type: 'event'\n  },\n  { anonymous: false, inputs: [], name: 'DisableTrustedOnly', type: 'event' },\n  { anonymous: false, inputs: [], name: 'EIP712DomainChanged', type: 'event' },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'uint256',\n        name: 'keysMigratedAt',\n        type: 'uint256'\n      }\n    ],\n    name: 'Migrated',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address'\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address'\n      }\n    ],\n    name: 'OwnershipTransferStarted',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address'\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address'\n      }\n    ],\n    name: 'OwnershipTransferred',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'account',\n        type: 'address'\n      }\n    ],\n    name: 'Paused',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      { indexed: true, internalType: 'uint256', name: 'fid', type: 'uint256' },\n      { indexed: true, internalType: 'bytes', name: 'key', type: 'bytes' },\n      {\n        indexed: false,\n        internalType: 'bytes',\n        name: 'keyBytes',\n        type: 'bytes'\n      }\n    ],\n    name: 'Remove',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'oldIdRegistry',\n        type: 'address'\n      },\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'newIdRegistry',\n        type: 'address'\n      }\n    ],\n    name: 'SetIdRegistry',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'oldCaller',\n        type: 'address'\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newCaller',\n        type: 'address'\n      },\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'owner',\n        type: 'address'\n      }\n    ],\n    name: 'SetTrustedCaller',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'uint32',\n        name: 'keyType',\n        type: 'uint32'\n      },\n      {\n        indexed: false,\n        internalType: 'uint8',\n        name: 'metadataType',\n        type: 'uint8'\n      },\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'oldValidator',\n        type: 'address'\n      },\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'newValidator',\n        type: 'address'\n      }\n    ],\n    name: 'SetValidator',\n    type: 'event'\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'account',\n        type: 'address'\n      }\n    ],\n    name: 'Unpaused',\n    type: 'event'\n  },\n  {\n    inputs: [],\n    name: 'ADD_TYPEHASH',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'REMOVE_TYPEHASH',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'VERSION',\n    outputs: [{ internalType: 'string', name: '', type: 'string' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'acceptOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' },\n      { internalType: 'bytes', name: 'metadata', type: 'bytes' }\n    ],\n    name: 'add',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'fidOwner', type: 'address' },\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' },\n      { internalType: 'bytes', name: 'metadata', type: 'bytes' },\n      { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' }\n    ],\n    name: 'addFor',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      {\n        components: [\n          { internalType: 'uint256', name: 'fid', type: 'uint256' },\n          {\n            components: [\n              { internalType: 'bytes', name: 'key', type: 'bytes' },\n              { internalType: 'bytes', name: 'metadata', type: 'bytes' }\n            ],\n            internalType: 'struct IKeyRegistry.BulkAddKey[]',\n            name: 'keys',\n            type: 'tuple[]'\n          }\n        ],\n        internalType: 'struct IKeyRegistry.BulkAddData[]',\n        name: 'items',\n        type: 'tuple[]'\n      }\n    ],\n    name: 'bulkAddKeysForMigration',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      {\n        components: [\n          { internalType: 'uint256', name: 'fid', type: 'uint256' },\n          { internalType: 'bytes[]', name: 'keys', type: 'bytes[]' }\n        ],\n        internalType: 'struct IKeyRegistry.BulkResetData[]',\n        name: 'items',\n        type: 'tuple[]'\n      }\n    ],\n    name: 'bulkResetKeysForMigration',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'disableTrustedOnly',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'domainSeparatorV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'eip712Domain',\n    outputs: [\n      { internalType: 'bytes1', name: 'fields', type: 'bytes1' },\n      { internalType: 'string', name: 'name', type: 'string' },\n      { internalType: 'string', name: 'version', type: 'string' },\n      { internalType: 'uint256', name: 'chainId', type: 'uint256' },\n      { internalType: 'address', name: 'verifyingContract', type: 'address' },\n      { internalType: 'bytes32', name: 'salt', type: 'bytes32' },\n      { internalType: 'uint256[]', name: 'extensions', type: 'uint256[]' }\n    ],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'gracePeriod',\n    outputs: [{ internalType: 'uint24', name: '', type: 'uint24' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'bytes32', name: 'structHash', type: 'bytes32' }],\n    name: 'hashTypedDataV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'idRegistry',\n    outputs: [\n      { internalType: 'contract IdRegistryLike', name: '', type: 'address' }\n    ],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'isMigrated',\n    outputs: [{ internalType: 'bool', name: '', type: 'bool' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'uint256', name: 'fid', type: 'uint256' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' }\n    ],\n    name: 'keyDataOf',\n    outputs: [\n      {\n        components: [\n          {\n            internalType: 'enum IKeyRegistry.KeyState',\n            name: 'state',\n            type: 'uint8'\n          },\n          { internalType: 'uint32', name: 'keyType', type: 'uint32' }\n        ],\n        internalType: 'struct IKeyRegistry.KeyData',\n        name: '',\n        type: 'tuple'\n      }\n    ],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'uint256', name: 'fid', type: 'uint256' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' }\n    ],\n    name: 'keys',\n    outputs: [\n      {\n        internalType: 'enum IKeyRegistry.KeyState',\n        name: 'state',\n        type: 'uint8'\n      },\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' }\n    ],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'keysMigratedAt',\n    outputs: [{ internalType: 'uint40', name: '', type: 'uint40' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'migrateKeys',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'owner', type: 'address' }],\n    name: 'nonces',\n    outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'owner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'pause',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'paused',\n    outputs: [{ internalType: 'bool', name: '', type: 'bool' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'pendingOwner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'bytes', name: 'key', type: 'bytes' }],\n    name: 'remove',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'fidOwner', type: 'address' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' },\n      { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n      { internalType: 'bytes', name: 'sig', type: 'bytes' }\n    ],\n    name: 'removeFor',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'renounceOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: '_idRegistry', type: 'address' }],\n    name: 'setIdRegistry',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: '_trustedCaller', type: 'address' }\n    ],\n    name: 'setTrustedCaller',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' },\n      {\n        internalType: 'contract IMetadataValidator',\n        name: 'validator',\n        type: 'address'\n      }\n    ],\n    name: 'setValidator',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],\n    name: 'transferOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'address', name: 'fidOwner', type: 'address' },\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' },\n      { internalType: 'bytes', name: 'metadata', type: 'bytes' }\n    ],\n    name: 'trustedAdd',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'trustedCaller',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'trustedOnly',\n    outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],\n    stateMutability: 'view',\n    type: 'function'\n  },\n  {\n    inputs: [],\n    name: 'unpause',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function'\n  },\n  {\n    inputs: [\n      { internalType: 'uint32', name: 'keyType', type: 'uint32' },\n      { internalType: 'uint8', name: 'metadataType', type: 'uint8' }\n    ],\n    name: 'validators',\n    outputs: [\n      {\n        internalType: 'contract IMetadataValidator',\n        name: 'validator',\n        type: 'address'\n      }\n    ],\n    stateMutability: 'view',\n    type: 'function'\n  }\n] as const;\n\nexport const KEY_REGISTRY = {\n  address: KEY_REGISTRY_ADDRESS,\n  abi: KEY_REGISTRY_ABI\n};\n"
  },
  {
    "path": "src/contracts/validator.ts",
    "content": "const VALIDATOR_ABI = [\n  {\n    inputs: [\n      { internalType: 'address', name: '_idRegistry', type: 'address' },\n      { internalType: 'address', name: '_initialOwner', type: 'address' },\n    ],\n    stateMutability: 'nonpayable',\n    type: 'constructor',\n  },\n  { inputs: [], name: 'InvalidShortString', type: 'error' },\n  {\n    inputs: [{ internalType: 'string', name: 'str', type: 'string' }],\n    name: 'StringTooLong',\n    type: 'error',\n  },\n  { anonymous: false, inputs: [], name: 'EIP712DomainChanged', type: 'event' },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address',\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address',\n      },\n    ],\n    name: 'OwnershipTransferStarted',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'previousOwner',\n        type: 'address',\n      },\n      {\n        indexed: true,\n        internalType: 'address',\n        name: 'newOwner',\n        type: 'address',\n      },\n    ],\n    name: 'OwnershipTransferred',\n    type: 'event',\n  },\n  {\n    anonymous: false,\n    inputs: [\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'oldIdRegistry',\n        type: 'address',\n      },\n      {\n        indexed: false,\n        internalType: 'address',\n        name: 'newIdRegistry',\n        type: 'address',\n      },\n    ],\n    name: 'SetIdRegistry',\n    type: 'event',\n  },\n  {\n    inputs: [],\n    name: 'METADATA_TYPEHASH',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'VERSION',\n    outputs: [{ internalType: 'string', name: '', type: 'string' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'acceptOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'domainSeparatorV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'eip712Domain',\n    outputs: [\n      { internalType: 'bytes1', name: 'fields', type: 'bytes1' },\n      { internalType: 'string', name: 'name', type: 'string' },\n      { internalType: 'string', name: 'version', type: 'string' },\n      { internalType: 'uint256', name: 'chainId', type: 'uint256' },\n      { internalType: 'address', name: 'verifyingContract', type: 'address' },\n      { internalType: 'bytes32', name: 'salt', type: 'bytes32' },\n      { internalType: 'uint256[]', name: 'extensions', type: 'uint256[]' },\n    ],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [\n      {\n        components: [\n          { internalType: 'uint256', name: 'requestFid', type: 'uint256' },\n          { internalType: 'address', name: 'requestSigner', type: 'address' },\n          { internalType: 'bytes', name: 'signature', type: 'bytes' },\n          { internalType: 'uint256', name: 'deadline', type: 'uint256' },\n        ],\n        internalType:\n          'struct SignedKeyRequestValidator.SignedKeyRequestMetadata',\n        name: 'metadata',\n        type: 'tuple',\n      },\n    ],\n    name: 'encodeMetadata',\n    outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],\n    stateMutability: 'pure',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'bytes32', name: 'structHash', type: 'bytes32' }],\n    name: 'hashTypedDataV4',\n    outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'idRegistry',\n    outputs: [\n      { internalType: 'contract IdRegistryLike', name: '', type: 'address' },\n    ],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'owner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'pendingOwner',\n    outputs: [{ internalType: 'address', name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n  {\n    inputs: [],\n    name: 'renounceOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'address', name: '_idRegistry', type: 'address' }],\n    name: 'setIdRegistry',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],\n    name: 'transferOwnership',\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    inputs: [\n      { internalType: 'uint256', name: '', type: 'uint256' },\n      { internalType: 'bytes', name: 'key', type: 'bytes' },\n      { internalType: 'bytes', name: 'signedKeyRequestBytes', type: 'bytes' },\n    ],\n    name: 'validate',\n    outputs: [{ internalType: 'bool', name: '', type: 'bool' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n]\n"
  },
  {
    "path": "src/lib/api/auth.ts",
    "content": "export const AUTH: Readonly<RequestInit> = {\n  headers: {\n    Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN as string}`\n  }\n};\n"
  },
  {
    "path": "src/lib/api/trends.ts",
    "content": "import useSWR from 'swr';\nimport type { SWRConfiguration } from 'swr';\nimport type { FilteredTrends, SuccessResponse } from '@lib/types/place';\n\ntype SwrHooksReturn = {\n  loading: boolean;\n  error: Error | undefined;\n};\n\ntype UseTrendsReturn = SwrHooksReturn & {\n  data: SuccessResponse | undefined;\n};\n\ntype FilteredSuccessResponse = Omit<SuccessResponse, 'trends'> & {\n  trends: FilteredTrends;\n};\n\ntype FilteredUseTrendsReturn = SwrHooksReturn & {\n  data: FilteredSuccessResponse | undefined;\n};\n\nexport function useTrends(\n  id: number,\n  limit?: undefined,\n  config?: SWRConfiguration\n): UseTrendsReturn;\n\nexport function useTrends(\n  id: number,\n  limit: number,\n  config?: SWRConfiguration\n): FilteredUseTrendsReturn;\n\nexport function useTrends(\n  id: number,\n  limit?: number,\n  config?: SWRConfiguration\n): UseTrendsReturn | FilteredUseTrendsReturn {\n  const { data, error } = useSWR<SuccessResponse, Error>(\n    `/api/trends/place/${id}${limit ? `?limit=${limit}` : ''}`,\n    config\n  );\n\n  if (data && 'errors' in data)\n    return {\n      data: undefined,\n      loading: false,\n      error: new Error('Sorry we could not find any trends for this place')\n    };\n\n  return {\n    data,\n    error: error,\n    loading: !error && !data\n  };\n}\n"
  },
  {
    "path": "src/lib/chains/resolve-chain-icon.ts",
    "content": "import { LRU } from '../lru-cache';\n\nexport async function resolveChainIcon(chainId: number) {\n  const cacheName = `eip155-${chainId}`;\n  const cache = LRU.get(`eip155-${chainId}`);\n  if (cache) {\n    return cache;\n  }\n\n  const icon = await _resolveChainIcon(chainId);\n  LRU.set(cacheName, icon);\n  return icon;\n}\n\nexport async function _resolveChainIcon(chainId: number) {\n  const chain = await fetch(\n    `https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/chains/eip155-${chainId}.json`\n  );\n  if (!chain.ok) {\n    return null;\n  }\n  const chainJson = await chain.json();\n\n  const iconRes = await fetch(\n    `https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/icons/${chainJson.icon}.json`\n  );\n  if (!iconRes.ok) {\n    return null;\n  }\n  const icons = await iconRes.json();\n  let { url } = icons[0];\n\n  if (url) {\n    if (url.startsWith('ipfs://')) {\n      url = url.replace('ipfs://', 'https://ipfs.io/ipfs/');\n    }\n  }\n\n  return url || null;\n}\n"
  },
  {
    "path": "src/lib/context/auth-context.tsx",
    "content": "import { getRandomId } from '@lib/random';\nimport type { Bookmark } from '@lib/types/bookmark';\nimport type { UserFull, UserFullResponse, UserResponse } from '@lib/types/user';\nimport type { ReactNode } from 'react';\nimport { createContext, useContext, useEffect, useMemo, useState } from 'react';\nimport useSWR from 'swr';\nimport { WarpcastSignInModal } from '../../components/modal/sign-in-modal-warpcast';\nimport { fetchJSON } from '../fetch';\nimport { useModal } from '../hooks/useModal';\nimport {\n  ACTIVE_KEYPAIR_KEY,\n  addKeyPair,\n  getActiveKeyPair,\n  getKeyPairs,\n  removeKeyPair,\n  setKeyPair\n} from '../keys';\nimport { KeyPair } from '../types/keypair';\nimport { NotificationsResponseSummary } from '../types/notifications';\nimport { useRouter } from 'next/router';\n\nexport type UserWithKey = UserFull & { keyPair?: KeyPair };\n\ntype AuthContext = {\n  user: UserWithKey | null;\n  usersWithKeys: UserWithKey[];\n  error: Error | null;\n  loading: boolean;\n  isAdmin: boolean;\n  randomSeed: string;\n  userBookmarks: Bookmark[] | null;\n  userNotifications: number | null;\n  lastCheckedNotifications: Date | null;\n  timelineCursor: Date | null;\n  setTimelineCursor: (date: Date | null) => void;\n  signOut: () => Promise<void>;\n  showAddAccountModal: () => void;\n  setUser: (user: UserWithKey) => void;\n  handleUserAuth: (forceKeyPair?: KeyPair) => void;\n  resetNotifications: () => void;\n};\n\nexport const AuthContext = createContext<AuthContext | null>(null);\n\ntype AuthContextProviderProps = {\n  children: ReactNode;\n};\n\nexport function AuthContextProvider({\n  children\n}: AuthContextProviderProps): JSX.Element {\n  const router = useRouter();\n\n  const [user, setUser] = useState<UserWithKey | null>(null);\n  const [users, setUsers] = useState<UserWithKey[]>([]);\n  const [userBookmarks, setUserBookmarks] = useState<Bookmark[] | null>(null);\n  const [error, setError] = useState<Error | null>(null);\n  const [loading, setLoading] = useState(true);\n\n  const modal = useModal();\n\n  const [lastCheckedNotifications, setLastCheckedNotifications] =\n    useState<Date | null>(null);\n\n  const [timelineCursor, setTimelineCursor] = useState<Date | null>(null);\n\n  /**\n   * Key storage explainer:\n   * 'keyPair' storage is used to store the key pair of the currently signed in user.\n   * 'keyPairs' storage is used to store all key pairs that have been used to sign in.\n   */\n\n  const fetchUserForKey = async (\n    keyPair: KeyPair\n  ): Promise<UserFull | null> => {\n    const { result: user } = await fetchJSON<UserFullResponse>(\n      `/api/signer/${keyPair.publicKey}/user`\n    );\n    return (user as UserFull) || null;\n  };\n\n  const manageUser = async ({\n    keyPair,\n    id\n  }: {\n    keyPair?: KeyPair;\n    id?: string;\n  }): Promise<void> => {\n    let fetchedUser: UserFull | null = null;\n    if (keyPair) {\n      fetchedUser = await fetchUserForKey(keyPair);\n    } else if (id) {\n      const { result } = await fetchJSON<UserResponse>(`/api/user/${id}`);\n      fetchedUser = result as UserFull;\n    }\n\n    if (fetchedUser) setUser({ ...fetchedUser, keyPair });\n\n    setLoading(false);\n  };\n\n  /**\n   * Updates users and current user\n   * @param forceKeyPair Force a key pair to be set as the current user\n   */\n  const handleUserAuth = async (forceKeyPair?: KeyPair): Promise<void> => {\n    setLoading(true);\n\n    // Get signer from local storage\n    if (forceKeyPair) {\n      setKeyPair(forceKeyPair);\n    }\n\n    let keyPair = forceKeyPair || (await getActiveKeyPair());\n    const keyPairs = await getKeyPairs();\n\n    if (keyPair) {\n      void manageUser({ keyPair });\n    } else {\n      // Default to fid 3 view-only account\n      void manageUser({ id: '3' });\n      return;\n    }\n\n    // Add key pair to storage in case it's not already there\n    addKeyPair(keyPair);\n\n    // Fetch users for all key pairs\n    Promise.all(keyPairs.map(fetchUserForKey)).then((users) => {\n      const usersWithKeys = users\n        .map((user, index) =>\n          user ? { ...user, keyPair: keyPairs[index] } : null\n        )\n        .filter((user) => user !== null);\n      setUsers(usersWithKeys as UserWithKey[]);\n    });\n\n    // Go to /home if user is on /login\n    if (router.pathname === '/login' || router.pathname === '/')\n      router.push('/home');\n  };\n\n  useEffect(() => {\n    // `user` is changed by the user selection menu\n    // When it changes we need to update the current user in local storage\n    if (user?.keyPair) {\n      getActiveKeyPair().then((activeKeyPair) => {\n        if (\n          (!activeKeyPair ||\n            activeKeyPair.publicKey !== user.keyPair?.publicKey) &&\n          user.keyPair\n        ) {\n          setKeyPair(user.keyPair);\n        }\n      });\n    }\n  }, [user]);\n\n  useEffect(() => {\n    handleUserAuth();\n    setLastCheckedNotifications(\n      new Date(localStorage.getItem('lastChecked') || new Date().toISOString())\n    );\n    setTimelineCursor(new Date());\n  }, []);\n\n  const signOut = async (): Promise<void> => {\n    try {\n      const keyPair = await getActiveKeyPair();\n      if (!keyPair) throw new Error('No key pair found');\n\n      localStorage.removeItem(ACTIVE_KEYPAIR_KEY);\n      removeKeyPair(keyPair);\n      handleUserAuth();\n    } catch (error) {\n      setError(error as Error);\n    }\n  };\n\n  const isAdmin = false;\n  const randomSeed = useMemo(getRandomId, [user?.id]);\n\n  const { data: userNotifications, isValidating: loadingNotifications } =\n    useSWR(\n      router.pathname !== '/notifications' &&\n        user?.keyPair &&\n        lastCheckedNotifications\n        ? `/api/user/${\n            user.id\n          }/notifications?last_time=${lastCheckedNotifications.toISOString()}`\n        : null,\n      async (url) =>\n        (await fetchJSON<NotificationsResponseSummary>(url)).result,\n      {\n        revalidateOnFocus: false,\n        revalidateOnReconnect: false,\n        refreshWhenHidden: true,\n        refreshInterval: 10000 // Poll every 10 seconds\n      }\n    );\n\n  const resetNotifications = (): void => {\n    setLastCheckedNotifications(new Date());\n  };\n\n  useEffect(() => {\n    if (lastCheckedNotifications)\n      localStorage.setItem(\n        'lastChecked',\n        lastCheckedNotifications.toISOString()\n      );\n  }, [lastCheckedNotifications]);\n\n  const value: AuthContext = {\n    user,\n    usersWithKeys: users,\n    setUser,\n    error,\n    loading,\n    isAdmin,\n    randomSeed,\n    userBookmarks,\n    userNotifications: userNotifications?.badgeCount || null,\n    timelineCursor,\n    setTimelineCursor,\n    signOut,\n    showAddAccountModal: modal.openModal,\n    handleUserAuth,\n    resetNotifications,\n    lastCheckedNotifications\n  };\n\n  return (\n    <AuthContext.Provider value={value}>\n      <>\n        <WarpcastSignInModal {...modal}></WarpcastSignInModal>\n      </>\n      {children}\n    </AuthContext.Provider>\n  );\n}\n\nexport function useAuth(): AuthContext {\n  const context = useContext(AuthContext);\n\n  if (!context)\n    throw new Error('useAuth must be used within an AuthContextProvider');\n\n  return context;\n}\n"
  },
  {
    "path": "src/lib/context/theme-context.tsx",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport { useState, useEffect, createContext, useContext } from 'react';\nimport { useAuth } from './auth-context';\nimport type { ReactNode, ChangeEvent } from 'react';\nimport type { Theme, Accent } from '@lib/types/theme';\n\ntype ThemeContext = {\n  theme: Theme;\n  accent: Accent;\n  changeTheme: ({ target: { value } }: ChangeEvent<HTMLInputElement>) => void;\n  changeAccent: ({ target: { value } }: ChangeEvent<HTMLInputElement>) => void;\n};\n\nexport const ThemeContext = createContext<ThemeContext | null>(null);\n\ntype ThemeContextProviderProps = {\n  children: ReactNode;\n};\n\nfunction setInitialTheme(): Theme {\n  if (typeof window === 'undefined') return 'dark';\n\n  const savedTheme = localStorage.getItem('theme') as Theme | null;\n  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n\n  return savedTheme ?? (prefersDark ? 'dark' : 'light');\n}\n\nfunction setInitialAccent(): Accent {\n  if (typeof window === 'undefined') return 'blue';\n\n  const savedAccent = localStorage.getItem('accent') as Accent | null;\n\n  return savedAccent ?? 'blue';\n}\n\nexport function ThemeContextProvider({\n  children\n}: ThemeContextProviderProps): JSX.Element {\n  const [theme, setTheme] = useState<Theme>(setInitialTheme);\n  const [accent, setAccent] = useState<Accent>(setInitialAccent);\n\n  const { user } = useAuth();\n  const { id: userId, theme: userTheme, accent: userAccent } = user ?? {};\n\n  useEffect(() => {\n    if (user && userTheme) setTheme(userTheme);\n  }, [userId, userTheme]);\n\n  useEffect(() => {\n    if (user && userAccent) setAccent(userAccent);\n  }, [userId, userAccent]);\n\n  useEffect(() => {\n    const flipTheme = (theme: Theme): NodeJS.Timeout | undefined => {\n      const root = document.documentElement;\n      const targetTheme = theme === 'dim' ? 'dark' : theme;\n\n      if (targetTheme === 'dark') root.classList.add('dark');\n      else root.classList.remove('dark');\n\n      root.style.setProperty('--main-background', `var(--${theme}-background)`);\n\n      root.style.setProperty(\n        '--main-search-background',\n        `var(--${theme}-search-background)`\n      );\n\n      root.style.setProperty(\n        '--main-sidebar-background',\n        `var(--${theme}-sidebar-background)`\n      );\n\n      if (user) {\n        localStorage.setItem('theme', theme);\n        // return setTimeout(() => void updateUserTheme(user.id, { theme }), 500);\n      }\n\n      return undefined;\n    };\n\n    const timeoutId = flipTheme(theme);\n    return () => clearTimeout(timeoutId);\n  }, [userId, theme]);\n\n  useEffect(() => {\n    const flipAccent = (accent: Accent): NodeJS.Timeout | undefined => {\n      const root = document.documentElement;\n\n      root.style.setProperty('--main-accent', `var(--accent-${accent})`);\n\n      if (user) {\n        localStorage.setItem('accent', accent);\n        // return setTimeout(() => void updateUserTheme(user.id, { accent }), 500);\n      }\n\n      return undefined;\n    };\n\n    const timeoutId = flipAccent(accent);\n    return () => clearTimeout(timeoutId);\n  }, [userId, accent]);\n\n  const changeTheme = ({\n    target: { value }\n  }: ChangeEvent<HTMLInputElement>): void => setTheme(value as Theme);\n\n  const changeAccent = ({\n    target: { value }\n  }: ChangeEvent<HTMLInputElement>): void => setAccent(value as Accent);\n\n  const value: ThemeContext = {\n    theme,\n    accent,\n    changeTheme,\n    changeAccent\n  };\n\n  return (\n    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n  );\n}\n\nexport function useTheme(): ThemeContext {\n  const context = useContext(ThemeContext);\n\n  if (!context)\n    throw new Error('useTheme must be used within an ThemeContextProvider');\n\n  return context;\n}\n"
  },
  {
    "path": "src/lib/context/user-context.tsx",
    "content": "import { createContext, useContext } from 'react';\nimport type { ReactNode } from 'react';\nimport type { User, UserFull } from '@lib/types/user';\n\ntype UserContext = {\n  user: UserFull | null;\n  loading: boolean;\n};\n\nexport const UserContext = createContext<UserContext | null>(null);\n\ntype UserContextProviderProps = {\n  value: UserContext;\n  children: ReactNode;\n};\n\nexport function UserContextProvider({\n  value,\n  children\n}: UserContextProviderProps): JSX.Element {\n  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;\n}\n\nexport function useUser(): UserContext {\n  const context = useContext(UserContext);\n\n  if (!context)\n    throw new Error('useUser must be used within an UserContextProvider');\n\n  return context;\n}\n"
  },
  {
    "path": "src/lib/context/window-context.tsx",
    "content": "import { createContext, useContext, useState, useEffect } from 'react';\nimport type { ReactNode } from 'react';\n\ntype WindowSize = {\n  width: number;\n  height: number;\n};\n\ntype WindowContext = WindowSize & {\n  isMobile: boolean;\n};\n\nexport const WindowContext = createContext<WindowContext | null>(null);\n\ntype WindowContextProviderProps = {\n  children: ReactNode;\n};\nexport function WindowContextProvider({\n  children\n}: WindowContextProviderProps): JSX.Element {\n  const [windowSize, setWindowSize] = useState<WindowSize>({\n    width: typeof window !== 'undefined' ? window.innerWidth : 9999,\n    height: typeof window !== 'undefined' ? window.innerHeight : 9999\n  });\n\n  const [isMobile, setIsMobile] = useState<boolean>(false);\n\n  useEffect(() => {\n    const handleResize = (): void => {\n      const width = window.innerWidth;\n      const height = window.innerHeight;\n\n      setWindowSize({\n        width,\n        height\n      });\n\n      setIsMobile(width < 500);\n    };\n\n    handleResize(); // Initially set the size and isMobile\n\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  const value: WindowContext = {\n    ...windowSize,\n    isMobile\n  };\n\n  return (\n    <WindowContext.Provider value={value}>{children}</WindowContext.Provider>\n  );\n}\n\nexport function useWindow(): WindowContext {\n  const context = useContext(WindowContext);\n\n  if (!context)\n    throw new Error('useWindow must be used within an WindowContextProvider');\n\n  return context;\n}\n"
  },
  {
    "path": "src/lib/crypto.ts",
    "content": "import * as ed from '@noble/ed25519';\nimport { KeyPair } from './types/keypair';\nimport { bytesToHex } from 'viem';\n\nexport async function generateKeyPair(): Promise<KeyPair> {\n  const privateKey = ed.utils.randomPrivateKey();\n  const publicKey = await ed.getPublicKeyAsync(privateKey);\n\n  return {\n    publicKey: bytesToHex(publicKey),\n    privateKey: bytesToHex(privateKey)\n  };\n}\n\nexport async function getKeyPair(privateKey: `0x${string}`): Promise<KeyPair> {\n  const publicKey = await ed.getPublicKeyAsync(privateKey.slice(2));\n\n  return {\n    publicKey: bytesToHex(publicKey),\n    privateKey\n  };\n}\n"
  },
  {
    "path": "src/lib/date.ts",
    "content": "const RELATIVE_TIME_FORMATTER = new Intl.RelativeTimeFormat('en-gb', {\n  style: 'short',\n  numeric: 'auto'\n});\n\ntype Units = Readonly<Partial<Record<Intl.RelativeTimeFormatUnit, number>>>;\n\nconst UNITS: Units = {\n  day: 24 * 60 * 60 * 1000,\n  hour: 60 * 60 * 1000,\n  minute: 60 * 1000\n};\n\nexport function formatDate(\n  targetDate: Date,\n  mode: 'tweet' | 'message' | 'full' | 'joined'\n): string {\n  const date = targetDate;\n\n  if (mode === 'full') return getFullTime(date);\n  if (mode === 'tweet') return getPostTime(date);\n  if (mode === 'joined') return getJoinedTime(date);\n\n  return getShortTime(date);\n}\n\nexport function formatNumber(number: number): string {\n  return new Intl.NumberFormat('en-GB', {\n    notation: number > 10_000 ? 'compact' : 'standard',\n    maximumFractionDigits: 1\n  }).format(number);\n}\n\nfunction getFullTime(date: Date): string {\n  const fullDate = new Intl.DateTimeFormat('en-gb', {\n    hour: 'numeric',\n    minute: 'numeric',\n    day: 'numeric',\n    month: 'short',\n    year: 'numeric'\n  }).format(date);\n\n  let splittedDate = fullDate.split(', ');\n\n  // Safari workaround\n  if (splittedDate.length === 1) splittedDate = fullDate.split(' at ');\n\n  const formattedDate =\n    splittedDate.length === 2\n      ? [...splittedDate].reverse().join(' · ')\n      : [splittedDate.slice(0, 2).join(', '), splittedDate.slice(-1)]\n          .reverse()\n          .join(' · ');\n\n  return formattedDate;\n}\n\nfunction getPostTime(date: Date): string {\n  if (isToday(date)) return getRelativeTime(date);\n  if (isYesterday(date))\n    return new Intl.DateTimeFormat('en-gb', {\n      day: 'numeric',\n      month: 'short'\n    }).format(date);\n\n  return new Intl.DateTimeFormat('en-gb', {\n    day: 'numeric',\n    month: 'short',\n    year: isCurrentYear(date) ? undefined : 'numeric'\n  }).format(date);\n}\n\nfunction getJoinedTime(date: Date): string {\n  return new Intl.DateTimeFormat('en-gb', {\n    month: 'long',\n    year: 'numeric'\n  }).format(date);\n}\n\nfunction getShortTime(date: Date): string {\n  const isNear = isToday(date)\n    ? 'today'\n    : isYesterday(date)\n    ? 'yesterday'\n    : null;\n\n  return isNear\n    ? `${isNear === 'today' ? 'Today' : 'Yesterday'} at ${date\n        .toLocaleTimeString('en-gb')\n        .slice(0, -3)}`\n    : getFullTime(date);\n}\n\nfunction getRelativeTime(date: Date): string {\n  const relativeTime = calculateRelativeTime(date);\n\n  if (relativeTime === 'now') return relativeTime;\n\n  const [number, unit] = relativeTime.split(' ');\n\n  return `${number}${unit[0]}`;\n}\n\nfunction calculateRelativeTime(date: Date): string {\n  const elapsed = +date - +new Date();\n\n  if (elapsed > 0) return 'now';\n\n  const unitsItems = Object.entries(UNITS) as [keyof Units, number][];\n\n  for (const [unit, millis] of unitsItems)\n    if (Math.abs(elapsed) > millis)\n      return RELATIVE_TIME_FORMATTER.format(Math.round(elapsed / millis), unit);\n\n  return RELATIVE_TIME_FORMATTER.format(Math.round(elapsed / 1000), 'second');\n}\n\nfunction isToday(date: Date): boolean {\n  // Less than 24 hours ago\n  return new Date().getTime() - date.getTime() < UNITS.day!;\n}\n\nfunction isYesterday(date: Date): boolean {\n  const yesterday = new Date();\n  yesterday.setDate(yesterday.getDate() - 1);\n  return yesterday.toDateString() === date.toDateString();\n}\n\nfunction isCurrentYear(date: Date): boolean {\n  return date.getFullYear() === new Date().getFullYear();\n}\n"
  },
  {
    "path": "src/lib/embeds.ts",
    "content": "import { getFrame } from 'frames.js';\r\nimport getMetaData from 'metadata-scraper';\r\nimport { LRU } from './lru-cache';\r\nimport { ExternalEmbed, Tweet } from './types/tweet';\r\n\r\nconst KNOWN_HOSTS_MAP: {\r\n  [key: string]: { urlBuilder?: (url: string) => string; userAgent?: string };\r\n} = {\r\n  'twitter.com': {\r\n    urlBuilder: (url: string) => url.replace('twitter.com', 'fxtwitter.com'),\r\n    userAgent: 'bot'\r\n  },\r\n  'x.com': {\r\n    urlBuilder: (url: string) => url.replace('x.com', 'fxtwitter.com'),\r\n    userAgent: 'bot'\r\n  },\r\n  'arxiv.org': {\r\n    userAgent:\r\n      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36'\r\n  }\r\n};\r\n\r\n// TODO: This method's inputs should be more generic\r\nexport async function populateEmbed(\r\n  embed: ExternalEmbed\r\n): Promise<ExternalEmbed | null> {\r\n  let host = '';\r\n  try {\r\n    host = new URL(embed.url).host;\r\n  } catch (e) {\r\n    console.error(`Error parsing URL ${embed.url}`);\r\n    return null;\r\n  }\r\n\r\n  const url = KNOWN_HOSTS_MAP[host]?.urlBuilder?.(embed.url) || embed.url;\r\n  const cached = LRU.get(url);\r\n  if (cached !== undefined) {\r\n    return cached;\r\n  }\r\n\r\n  let result: ExternalEmbed | null = null;\r\n\r\n  try {\r\n    const userAgent = KNOWN_HOSTS_MAP[host]?.userAgent || undefined;\r\n    // TODO:\r\n    // {\r\n    //     maxRedirects: 1,\r\n    //     timeout: 1000,\r\n    //     ua:\r\n    //       userAgent ||\r\n    //       'Mozilla/5.0 (compatible; TelegramBot/1.0; +https://core.telegram.org/bots/webhooks)'\r\n    //   }\r\n    const req = await fetch(url, {\r\n      headers: {\r\n        'User-Agent':\r\n          userAgent || 'Mozilla/5.0 (compatible; OpenframesBot/1.0;)'\r\n      }\r\n    });\r\n    const contentType = req.headers.get('content-type');\r\n    const html = await req.text();\r\n\r\n    const metadata = await getMetaData({\r\n      html\r\n    });\r\n    const { title, description, icon, image } = metadata;\r\n\r\n    const frameResult = getFrame({ htmlString: html, url });\r\n\r\n    if (!contentType) {\r\n      return null;\r\n    }\r\n\r\n    if (title || description || contentType.startsWith('image/')) {\r\n      const populatedEmbed: ExternalEmbed = {\r\n        url: embed.url,\r\n        title: title,\r\n        text: description,\r\n        icon: icon ? new URL(icon, url).toString() : undefined,\r\n        image: image ? new URL(image).toString() : undefined,\r\n        contentType,\r\n        frame: frameResult.status === 'success' ? frameResult.frame : undefined\r\n      };\r\n      LRU.set(url, populatedEmbed);\r\n      result = populatedEmbed;\r\n    }\r\n  } catch (e) {\r\n    console.log(\r\n      `Error fetching embed for ${url}`,\r\n      e instanceof Error ? e.message : e\r\n    );\r\n    // console.error(e);\r\n  }\r\n\r\n  // If we get an error, cache the error for an hour\r\n  LRU.set(url, result, { ttl: result === null ? 1000 * 60 * 60 : undefined });\r\n\r\n  // console.log(`Finished populating embed for ${embed.url}`, result);\r\n\r\n  return result;\r\n}\r\n\r\nexport async function populateTweetEmbeds(tweet: Tweet): Promise<Tweet> {\r\n  const populatedTweet: Tweet = {\r\n    ...tweet,\r\n    embeds: (await Promise.all(tweet.embeds.map(populateEmbed))).filter(\r\n      (embed) => embed !== null\r\n    ) as ExternalEmbed[]\r\n  };\r\n  return populatedTweet;\r\n}\r\n"
  },
  {
    "path": "src/lib/env.ts",
    "content": "export const isProduction = process.env.NODE_ENV === 'production';\nexport const isDevelopment = process.env.NODE_ENV === 'development';\n\nexport const siteURL = process.env.NEXT_PUBLIC_URL as string;\n"
  },
  {
    "path": "src/lib/farcaster/index.ts",
    "content": "import {\n  getInsecureHubRpcClient,\n  getSSLHubRpcClient,\n  HubRpcClient\n} from '@farcaster/hub-nodejs';\n\nconst globalForFarcaster = global as unknown as {\n  hubClient: HubRpcClient | undefined;\n};\n\nexport const hubClient =\n  globalForFarcaster.hubClient ??\n  (process.env.FC_HUB_USE_TLS && process.env.FC_HUB_USE_TLS !== 'false'\n    ? getSSLHubRpcClient(process.env.FC_HUB_URL!)\n    : getInsecureHubRpcClient(process.env.FC_HUB_URL!));\n\nif (process.env.NODE_ENV !== 'production')\n  globalForFarcaster.hubClient = hubClient;\n"
  },
  {
    "path": "src/lib/farcaster/utils.ts",
    "content": "import {\n  Embed,\n  FarcasterNetwork,\n  HashScheme,\n  makeCastAdd,\n  makeCastRemove,\n  makeLinkAdd,\n  makeLinkRemove,\n  makeReactionAdd,\n  makeReactionRemove,\n  Message,\n  MessageData,\n  NobleEd25519Signer,\n  ReactionType\n} from '@farcaster/hub-web';\nimport { blake3 } from '@noble/hashes/blake3';\nimport { getActiveKeyPair } from '../keys';\n\nfunction getSigner(privateKey: string): NobleEd25519Signer {\n  const ed25519Signer = new NobleEd25519Signer(Buffer.from(privateKey, 'hex'));\n  return ed25519Signer;\n}\n\nasync function getSignerFromStorage(): Promise<NobleEd25519Signer> {\n  const keyPair = await getActiveKeyPair();\n  const privateKey = keyPair?.privateKey.slice(2);\n\n  if (!privateKey) throw new Error('No signer found');\n\n  return getSigner(privateKey);\n}\n\nexport async function createReactionMessage({\n  castHash,\n  castAuthorFid,\n  type,\n  remove,\n  fid\n}: {\n  castHash: string;\n  castAuthorFid: number;\n  type: ReactionType;\n  remove?: boolean;\n  fid: number;\n}) {\n  const signer = await getSignerFromStorage();\n\n  const messageDataOptions = {\n    fid,\n    network: FarcasterNetwork.MAINNET\n  };\n\n  const maker = remove ? makeReactionRemove : makeReactionAdd;\n\n  const message = await maker(\n    {\n      type,\n      targetCastId: {\n        hash: new Uint8Array(Buffer.from(castHash, 'hex')),\n        fid: castAuthorFid\n      }\n    },\n    messageDataOptions,\n    signer\n  );\n\n  return message.unwrapOr(null);\n}\n\nexport async function createCastMessage({\n  text,\n  embeds,\n  parentCastHash,\n  parentCastFid,\n  parentUrl,\n  mentions,\n  mentionsPositions,\n  fid\n}: {\n  text: string;\n  embeds?: Embed[];\n  parentCastHash?: string;\n  parentCastFid?: number;\n  parentUrl?: string;\n  mentions?: number[];\n  mentionsPositions?: number[];\n  fid: number;\n}) {\n  const signer = await getSignerFromStorage();\n\n  const messageDataOptions = {\n    fid,\n    network: FarcasterNetwork.MAINNET\n  };\n\n  const parentCastId =\n    parentCastHash && parentCastFid\n      ? {\n          hash: new Uint8Array(Buffer.from(parentCastHash, 'hex')),\n          fid: parentCastFid\n        }\n      : undefined;\n\n  const message = await makeCastAdd(\n    {\n      text,\n      embeds: embeds || [],\n      embedsDeprecated: [],\n      mentions: mentions || [],\n      mentionsPositions: mentionsPositions || [],\n      parentCastId,\n      parentUrl\n    },\n    messageDataOptions,\n    signer\n  );\n\n  if (message.isErr()) {\n    console.error(message.error);\n  }\n\n  return message.unwrapOr(null);\n}\n\nexport async function createRemoveCastMessage({\n  castHash,\n  castAuthorFid\n}: {\n  castHash: string;\n  castAuthorFid: number;\n}) {\n  const signer = await getSignerFromStorage();\n  const messageDataOptions = {\n    fid: castAuthorFid,\n    network: FarcasterNetwork.MAINNET\n  };\n  const message = await makeCastRemove(\n    {\n      targetHash: new Uint8Array(Buffer.from(castHash, 'hex'))\n    },\n    messageDataOptions,\n    signer\n  );\n\n  return message.unwrapOr(null);\n}\n\nexport async function createFollowMessage({\n  targetFid,\n  fid,\n  remove\n}: {\n  targetFid: number;\n  fid: number;\n  remove?: boolean;\n}) {\n  const signer = await getSignerFromStorage();\n  const messageDataOptions = {\n    fid: fid,\n    network: FarcasterNetwork.MAINNET\n  };\n\n  const maker = remove ? makeLinkRemove : makeLinkAdd;\n\n  const message = await maker(\n    {\n      type: 'follow',\n      targetFid: targetFid\n    },\n    messageDataOptions,\n    signer\n  );\n\n  return message.unwrapOr(null);\n}\n\nexport async function makeMessage(messageData: MessageData) {\n  const signer = await getSignerFromStorage();\n\n  const dataBytes = MessageData.encode(messageData).finish();\n\n  const hash = blake3(dataBytes, { dkLen: 20 });\n\n  const signature = await signer.signMessageHash(hash);\n  if (signature.isErr()) return null;\n\n  const signerKey = await signer.getSignerKey();\n  if (signerKey.isErr()) return null;\n\n  const message = Message.create({\n    data: messageData,\n    hash,\n    hashScheme: HashScheme.BLAKE3,\n    signature: signature.value,\n    signatureScheme: signer.scheme,\n    signer: signerKey.value\n  });\n\n  return message;\n}\n\nexport async function submitHubMessage(message: Message) {\n  const res = await fetch('/api/hub', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify({ message: Message.toJSON(message) })\n  });\n  const { result: hubResult } = await res.json();\n  return hubResult;\n}\n\nexport async function batchSubmitHubMessages(messages: Message[]) {\n  const res = await fetch('/api/hub/batch', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify({ messages: messages.map(Message.toJSON) })\n  });\n  return res.ok;\n}\n"
  },
  {
    "path": "src/lib/fetch.ts",
    "content": "export async function fetchJSON<T>(\n  resource: RequestInfo,\n  init?: RequestInit | undefined\n): Promise<T> {\n  const response = await fetch(resource, init);\n  const data = (await response.json()) as T;\n\n  return data;\n}\n"
  },
  {
    "path": "src/lib/hooks/useConnectedWalletFid.tsx",
    "content": "import { useAccount, useReadContract } from 'wagmi';\nimport { ID_REGISTRY } from '../../contracts';\nimport { useEffect } from 'react';\n\nfunction useFid() {\n  const { address } = useAccount();\n\n  const { data, error, isLoading } = useReadContract({\n    ...ID_REGISTRY,\n    chainId: 10,\n    functionName: address ? 'idOf' : undefined,\n    args: address ? [address] : undefined\n  });\n\n  return { data, error, isLoading };\n}\n\nexport default useFid;\n"
  },
  {
    "path": "src/lib/hooks/useInfiniteScroll.tsx",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport { motion } from 'framer-motion';\nimport { useCallback, useEffect, useState } from 'react';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport { Loading } from '../../components/ui/loading';\nimport { PaginatedTweetsResponse } from '../paginated-tweets';\n\nexport function useInfiniteScroll(\n  urlBuilder: (pageParam: string | null) => string,\n  options?: {\n    initialSize?: number;\n    enabled?: boolean;\n    stepSize?: number;\n    marginBottom?: number;\n    queryKey?: any[];\n    refetchOnFocus?: boolean;\n  }\n) {\n  const {\n    initialSize,\n    stepSize,\n    marginBottom,\n    queryKey,\n    enabled,\n    refetchOnFocus\n  } = {\n    initialSize: 10,\n    stepSize: 10,\n    marginBottom: 1000,\n    queryKey: [],\n    ...(options ?? {})\n  };\n\n  const [reachedLimit, setReachedLimit] = useState(false);\n  const [loadMoreInView, setLoadMoreInView] = useState(false);\n\n  const fetchData = async ({ pageParam = null }) => {\n    if (!enabled) {\n      return;\n    }\n    const url = urlBuilder(pageParam);\n    const response = await fetch(url);\n    if (!response.ok) {\n      throw new Error('Could not fetch the casts');\n    }\n\n    const { result } = (await response.json()) as PaginatedTweetsResponse;\n\n    if (!result) {\n      throw new Error('Could not fetch the casts');\n      return;\n    }\n\n    return result;\n  };\n\n  const {\n    data,\n    fetchNextPage,\n    hasNextPage,\n    isLoading: loading,\n    isFetchingNextPage\n  } = useInfiniteQuery<PaginatedTweetsResponse['result']>({\n    queryKey,\n    queryFn: fetchData as any, // TODO: Fix this\n    getNextPageParam: (lastPage) => {\n      return lastPage?.nextPageCursor || false;\n    },\n    // getNextPageParam: (lastPage) => {\n    //   return lastPage?.nextPageCursor ?? false;\n    // },\n    enabled: enabled !== undefined ? enabled : true,\n    refetchOnWindowFocus: refetchOnFocus !== undefined ? refetchOnFocus : true,\n    initialPageParam: null\n  });\n\n  useEffect(() => {\n    if (reachedLimit) return;\n    if (loadMoreInView) {\n      fetchNextPage();\n    }\n  }, [loadMoreInView]);\n\n  const makeItInView = (): void => setLoadMoreInView(true);\n  const makeItNotInView = (): void => setLoadMoreInView(false);\n\n  const isLoadMoreHidden = !(hasNextPage || isFetchingNextPage);\n\n  const LoadMore = useCallback(\n    (): JSX.Element => (\n      <motion.div\n        className={isLoadMoreHidden ? 'hidden' : 'block'}\n        viewport={{ margin: `0px 0px ${marginBottom ?? 1000}px` }}\n        onViewportEnter={makeItInView}\n        onViewportLeave={makeItNotInView}\n      >\n        {loading && <Loading className='mt-5' />}\n      </motion.div>\n    ),\n    [isLoadMoreHidden, loading]\n  );\n\n  return { data, loading, LoadMore };\n}\n"
  },
  {
    "path": "src/lib/hooks/useInfiniteScrollUsers.tsx",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport { motion } from 'framer-motion';\nimport { useCallback, useEffect, useState } from 'react';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport { Loading } from '../../components/ui/loading';\nimport { PaginatedUsersResponse } from '../paginated-reactions';\n\nexport function useInfiniteScrollUsers(\n  urlBuilder: (pageParam: string | null) => string,\n  options?: {\n    initialSize?: number;\n    stepSize?: number;\n    marginBottom?: number;\n    queryKey?: any[];\n    enabled?: boolean;\n  }\n) {\n  const { initialSize, stepSize, marginBottom, queryKey, enabled } = {\n    initialSize: 10,\n    stepSize: 10,\n    marginBottom: 100,\n    queryKey: [],\n    enabled: true,\n    ...(options ?? {})\n  };\n\n  const [reachedLimit, setReachedLimit] = useState(false);\n  const [loadMoreInView, setLoadMoreInView] = useState(false);\n\n  const fetchData = async ({ pageParam = null }) => {\n    const url = urlBuilder(pageParam);\n    const response = await fetch(url);\n    if (!response.ok) {\n      throw new Error('Could not fetch the data');\n    }\n\n    const { result } = (await response.json()) as PaginatedUsersResponse;\n\n    if (!result) {\n      throw new Error('Could not fetch the data');\n      return;\n    }\n\n    const { users, nextPageCursor } = result;\n\n    return {\n      users,\n      nextPageCursor\n    };\n  };\n\n  const {\n    data,\n    fetchNextPage,\n    hasNextPage,\n    isLoading: loading,\n    isFetchingNextPage\n  } = useInfiniteQuery<PaginatedUsersResponse['result']>({\n    queryKey,\n    queryFn: fetchData as any, // TODO: Fix this\n    getNextPageParam: (lastPage) => {\n      return lastPage?.nextPageCursor ?? false;\n    },\n    initialPageParam: null,\n    enabled: enabled\n  });\n\n  useEffect(() => {\n    if (reachedLimit) return;\n    if (loadMoreInView) {\n      fetchNextPage();\n    }\n  }, [loadMoreInView]);\n\n  const makeItInView = (): void => setLoadMoreInView(true);\n  const makeItNotInView = (): void => setLoadMoreInView(false);\n\n  const isLoadMoreHidden = !(hasNextPage || isFetchingNextPage);\n\n  const LoadMore = useCallback(\n    (): JSX.Element => (\n      <motion.div\n        className={isLoadMoreHidden ? 'hidden' : 'block'}\n        viewport={{ margin: `0px 0px ${marginBottom ?? 1000}px` }}\n        onViewportEnter={makeItInView}\n        onViewportLeave={makeItNotInView}\n      >\n        <Loading className='mt-5' />\n      </motion.div>\n    ),\n    [isLoadMoreHidden]\n  );\n\n  return { data, loading, LoadMore };\n}\n"
  },
  {
    "path": "src/lib/hooks/useModal.ts",
    "content": "import { useState } from 'react';\n\ntype Modal = {\n  open: boolean;\n  openModal: () => void;\n  closeModal: () => void;\n};\n\nexport function useModal(): Modal {\n  const [open, setOpen] = useState(false);\n\n  const openModal = (): void => setOpen(true);\n  const closeModal = (): void => setOpen(false);\n\n  return { open, openModal, closeModal };\n}\n"
  },
  {
    "path": "src/lib/hooks/useRequireAuth.ts",
    "content": "import { useEffect } from 'react';\nimport { useRouter } from 'next/router';\nimport { useAuth } from '@lib/context/auth-context';\nimport type { User } from '@lib/types/user';\n\nexport function useRequireAuth(redirectUrl?: string): User | null {\n  const { user, loading } = useAuth();\n  const { replace } = useRouter();\n\n  useEffect(() => {\n    if (!loading && !user) void replace(redirectUrl ?? '/');\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [user, loading]);\n\n  return user;\n}\n"
  },
  {
    "path": "src/lib/imgur/upload.ts",
    "content": "export const uploadToImgur = async (file: File): Promise<string | null> => {\n  const formData = new FormData();\n  formData.append('image', file);\n  formData.append('type', 'file');\n  const response = await fetch('https://api.imgur.com/3/upload', {\n    method: 'POST',\n    headers: {\n      Authorization: `Client-ID ${process.env.NEXT_PUBLIC_IMGUR_CLIENT_ID!}` // replace with your Client ID\n    },\n    body: formData\n  });\n\n  if (!response.ok) {\n    return null;\n  }\n\n  const { data } = await response.json();\n\n  if (!data) return null;\n\n  return data.link;\n};\n"
  },
  {
    "path": "src/lib/keys.ts",
    "content": "import * as ed from '@noble/ed25519';\nimport { KeyPair } from './types/keypair';\nimport { bytesToHex } from 'viem';\n\nexport const KEYPAIRS_KEY = 'keys';\nexport const ACTIVE_KEYPAIR_KEY = 'activeKey';\nexport const PENDING_KEYPAIR_KEY = 'pendingKey';\n\nexport async function getKeyPairs() {\n  const keysRaw = localStorage.getItem(KEYPAIRS_KEY) as string | null;\n  const keys: `0x${string}`[] = keysRaw ? JSON.parse(keysRaw) : [];\n\n  const keyPairs = await Promise.all(\n    keys.map(async (privateKey) => ({\n      publicKey: bytesToHex(await ed.getPublicKeyAsync(privateKey.slice(2))),\n      privateKey\n    }))\n  );\n\n  return keyPairs;\n}\n\nexport function addKeyPair(keyPair: KeyPair) {\n  const keyPairsRaw = localStorage.getItem(KEYPAIRS_KEY) as string | null;\n  const keyPairs: `0x${string}`[] = keyPairsRaw ? JSON.parse(keyPairsRaw) : [];\n  if (!keyPairs.find((kp) => kp === keyPair.privateKey)) {\n    keyPairs.push(keyPair.privateKey);\n    localStorage.setItem(KEYPAIRS_KEY, JSON.stringify(keyPairs));\n  }\n}\n\nexport function removeKeyPair(keyPair: KeyPair) {\n  const keyPairsRaw = localStorage.getItem(KEYPAIRS_KEY) as string | null;\n  const keyPairs: `0x${string}`[] = keyPairsRaw ? JSON.parse(keyPairsRaw) : [];\n  const newKeyPairs = keyPairs.filter((kp) => kp !== keyPair.privateKey);\n  localStorage.setItem(KEYPAIRS_KEY, JSON.stringify(newKeyPairs));\n}\n\nexport function setKeyPair(keyPair: KeyPair) {\n  localStorage.setItem(ACTIVE_KEYPAIR_KEY, keyPair.privateKey);\n}\n\nexport async function getActiveKeyPair(): Promise<KeyPair | null> {\n  const privateKey = localStorage.getItem(ACTIVE_KEYPAIR_KEY) as\n    | `0x${string}`\n    | null;\n\n  if (!privateKey) {\n    const keyPairs = await getKeyPairs();\n    if (keyPairs.length > 0) {\n      setKeyPair(keyPairs[0]);\n      const keyPair = await getActiveKeyPair();\n      return keyPair;\n    } else {\n      return null;\n    }\n  }\n\n  const publicKey = await ed.getPublicKeyAsync(privateKey.slice(2));\n\n  return {\n    privateKey,\n    publicKey: bytesToHex(publicKey)\n  };\n}\n"
  },
  {
    "path": "src/lib/lru-cache.ts",
    "content": "import { LRUCache } from 'lru-cache';\n\nconst globalForLRU = global as unknown as {\n  LRU: LRUCache<string, any> | undefined;\n};\n\nexport const LRU = globalForLRU.LRU ?? new LRUCache({ max: 1000 });\n\nif (process.env.NODE_ENV !== 'production') globalForLRU.LRU = LRU;\n"
  },
  {
    "path": "src/lib/merge.ts",
    "content": "import type { Timestamp } from 'firebase/firestore';\n\ntype DataWithDate<T> = T & { createdAt: Timestamp };\n\nexport function mergeData<T>(\n  sortData: boolean,\n  ...tweets: (DataWithDate<T>[] | null)[]\n): DataWithDate<T>[] | null {\n  const validData = tweets.filter((tweet) => tweet) as DataWithDate<T>[][];\n  const mergeData = validData.reduce((acc, tweet) => [...acc, ...tweet], []);\n\n  return mergeData.length\n    ? sortData\n      ? mergeData.sort((a, b) => +b.createdAt.toDate() - +a.createdAt.toDate())\n      : mergeData\n    : null;\n}\n"
  },
  {
    "path": "src/lib/paginated-reactions.ts",
    "content": "import { Prisma } from '@prisma/client';\nimport { prisma } from './prisma';\nimport { BaseResponse } from './types/responses';\nimport { User } from './types/user';\nimport { resolveUsers } from './user/resolve-user';\n\nexport interface PaginatedUsersResponse\n  extends BaseResponse<{\n    users: User[];\n    nextPageCursor: string | null;\n  }> {}\n\nexport async function getReactionUsersPaginated(\n  findManyArgs: Prisma.reactionsFindManyArgs,\n  full: boolean = false\n) {\n  const reactions = await prisma.reactions.findMany(findManyArgs);\n\n  const fids = reactions.map((reaction) => reaction.fid);\n\n  const users = await resolveUsers([...fids], full);\n\n  const nextPageCursor =\n    reactions.length > 0\n      ? reactions[reactions.length - 1].timestamp.toISOString()\n      : null;\n\n  return {\n    users,\n    nextPageCursor\n  };\n}\n"
  },
  {
    "path": "src/lib/paginated-tweets.ts",
    "content": "import { ReactionType } from '@farcaster/hub-web';\nimport { casts, Prisma } from '@prisma/client';\nimport { Sql } from '@prisma/client/runtime/library';\nimport { prisma } from './prisma';\nimport { BaseResponse } from './types/responses';\nimport { Tweet, tweetConverter } from './types/tweet';\nimport { User, UserFull, UsersMapType } from './types/user';\nimport { resolveUsersMap } from './user/resolve-user';\nimport { hexToBytes } from 'viem';\n\nexport type PaginatedTweetsType = {\n  tweets: Tweet[];\n  nextPageCursor: string | null;\n  // fid -> User\n  users: UsersMapType<User | UserFull>;\n};\nexport interface PaginatedTweetsResponse\n  extends BaseResponse<PaginatedTweetsType> {}\n\nexport interface TweetsResponse extends BaseResponse<{ tweets: Tweet[] }> {}\n\n/**\n *\n * @param sql Sql query which uses the returned cursor and returns all the fields of casts table\n * @returns Promise<PaginatedTweets>\n */\nexport async function getTweetsPaginatedRawSql(sql: Sql, ...args: any[]) {\n  const casts = await prisma.$queryRaw<casts[]>(sql);\n  return convertAndCalculateCursor(casts, ...args);\n}\n\n/**\n *\n * @param findManyArgs Prisma.castsFindManyArgs\n * @returns Promise<PaginatedTweets>\n */\nexport async function getTweetsPaginatedPrismaArgs(\n  findManyArgs: Prisma.castsFindManyArgs,\n  ...args: any[]\n) {\n  const casts = await prisma.casts.findMany(findManyArgs);\n  return await convertAndCalculateCursor(casts, ...args);\n}\n\n/**\n *\n * @param casts Casts to be converted to tweets\n * @param calculateNextPageCursor Function to calculate the next page cursor\n * @returns PaginatedTweets\n */\nexport async function convertAndCalculateCursor(\n  casts: casts[],\n  calculateNextPageCursor?: (casts: casts[]) => string | null\n): Promise<PaginatedTweetsType> {\n  let { tweets, allCasts } = await castsToTweets(casts);\n\n  const fids: Set<bigint> = allCasts.reduce((acc: Set<bigint>, cur) => {\n    acc.add(cur.fid);\n    if (cur.parent_fid) acc.add(cur.parent_fid);\n    (cur.mentions as number[])?.forEach((mention) => acc.add(BigInt(mention)));\n    return acc;\n  }, new Set<bigint>());\n\n  const usersMap = await resolveUsersMap([...fids]);\n\n  const nextPageCursor =\n    calculateNextPageCursor?.(casts) ||\n    (casts.length > 0 ? casts[casts.length - 1].timestamp.toISOString() : null);\n\n  return {\n    tweets,\n    users: usersMap,\n    nextPageCursor\n  };\n}\n\ntype CastToTweetsReturnType = {\n  tweets: Tweet[];\n  /** Root casts */\n  casts: casts[];\n  castHashes: Buffer[];\n  /** All casts entities (root casts and quoted casts) */\n  allCasts: casts[];\n};\n\nexport async function castsToTweets(\n  castsOrHashes: Buffer[] | casts[],\n  options: {\n    castRecursionDepth: number;\n    maxCastRecursionDepth: number;\n    includeEngagements: boolean;\n    includeReactions: boolean;\n    includeReplies: boolean;\n  } = {\n    castRecursionDepth: 0,\n    maxCastRecursionDepth: 1,\n    includeEngagements: true,\n    includeReactions: true,\n    includeReplies: true\n  }\n): Promise<CastToTweetsReturnType> {\n  const casts =\n    castsOrHashes[0] instanceof Buffer\n      ? await prisma.casts.findMany({\n          where: {\n            hash: {\n              in: castsOrHashes as Buffer[]\n            }\n          }\n        })\n      : (castsOrHashes as casts[]);\n  const allCasts = [...casts];\n\n  const castHashes =\n    castsOrHashes[0] instanceof Buffer\n      ? (castsOrHashes as Buffer[])\n      : casts.map((cast) => cast.hash);\n\n  const castEmbedsHashes: Set<Buffer> = new Set();\n\n  const castEmbedsHashesByCast = casts.reduce(\n    (acc: Record<string, string[]>, cast) => {\n      acc[cast.hash.toString('hex')] = (cast.embeds as any[])\n        .filter((embed) => 'castId' in embed)\n        .map((embed) => {\n          const hash = Buffer.from(hexToBytes(embed.castId.hash));\n          castEmbedsHashes.add(hash);\n          return hash.toString('hex');\n        });\n\n      return acc;\n    },\n    {}\n  );\n\n  let embeddedTweets: CastToTweetsReturnType | undefined = undefined;\n\n  if (options.castRecursionDepth < options.maxCastRecursionDepth) {\n    // Get contents of casts that are embedded\n    const castEmbeds = await prisma.casts.findMany({\n      where: {\n        hash: {\n          in: [...castEmbedsHashes]\n        }\n      }\n    });\n\n    embeddedTweets = await castsToTweets(castEmbeds, {\n      castRecursionDepth: options.castRecursionDepth + 1,\n      maxCastRecursionDepth: options.maxCastRecursionDepth,\n      includeEngagements: false,\n      includeReactions: false,\n      includeReplies: false\n    });\n\n    embeddedTweets.casts.forEach((c) => allCasts.push(c));\n  }\n\n  const engagements = options.includeEngagements\n    ? await prisma.reactions.findMany({\n        where: {\n          target_cast_hash: {\n            in: castHashes\n          },\n          deleted_at: null\n        },\n        select: {\n          fid: true,\n          type: true,\n          target_cast_hash: true\n        }\n      })\n    : [];\n\n  const replyCount = options.includeReplies\n    ? await prisma.casts.groupBy({\n        by: ['parent_hash'],\n        where: {\n          parent_hash: {\n            in: castHashes\n          },\n          deleted_at: null\n        },\n        _count: {\n          parent_hash: true\n        }\n      })\n    : [];\n\n  // Create a map of parent_hash to reply count\n  const replyCountMap = replyCount.reduce((acc: any, cur) => {\n    const key = cur.parent_hash!.toString('hex');\n    if (acc[key]) {\n      acc[key] = cur._count.parent_hash;\n    } else {\n      acc[key] = cur._count.parent_hash;\n    }\n    return acc;\n  }, {});\n\n  // Group reactions by reaction_type for each target_hash\n  const reactionsMap = engagements.reduce(\n    (acc: { [key: string]: { [key: number]: string[] } }, cur) => {\n      const key = cur.target_cast_hash!.toString('hex');\n      if (!key) {\n        return acc;\n      }\n      if (acc[key]) {\n        if (acc[key][cur.type]) {\n          acc[key][cur.type] = [...acc[key][cur.type], cur.fid.toString()];\n        } else {\n          acc[key][cur.type] = [cur.fid.toString()];\n        }\n      } else {\n        acc[key] = {\n          [cur.type]: [cur.fid.toString()]\n        };\n      }\n      return acc;\n    },\n    {}\n  );\n\n  // Merge the casts with the reactions\n  const tweets = casts.map((cast): Tweet => {\n    const id = cast.hash.toString('hex');\n\n    const quoteTweets = castEmbedsHashesByCast[id]\n      ?.map(\n        (id) => embeddedTweets?.tweets.find((tweet) => tweet.id === id) || null\n      )\n      .filter(Boolean) as Tweet[];\n    return {\n      ...tweetConverter.toTweet(cast),\n      userLikes: reactionsMap[id]\n        ? reactionsMap[id][ReactionType.LIKE] || []\n        : [],\n      userRetweets: reactionsMap[id]\n        ? reactionsMap[id][ReactionType.RECAST] || []\n        : [],\n      userReplies: replyCountMap[id] || 0,\n      quoteTweets: quoteTweets\n    };\n  });\n\n  return { tweets, casts, castHashes, allCasts };\n}\n"
  },
  {
    "path": "src/lib/passkeys.ts",
    "content": "import type { UserFull } from './types/user';\nimport { KeyPair } from './types/keypair';\nimport { hexToBytes } from 'viem';\n\ntype UserWithKey = UserFull & { keyPair?: KeyPair };\n\nexport async function storeSignerLargeBlob({\n  privateKey,\n  onSignerStored\n}: {\n  privateKey: `0x${string}`;\n  onSignerStored: (created: boolean) => void;\n}) {\n  try {\n    const challenge = new Uint8Array(32);\n    const blob = hexToBytes(privateKey);\n\n    let credential = await navigator.credentials.get({\n      publicKey: {\n        challenge,\n        extensions: {\n          // @ts-ignore -- LargeBlob extension is not yet in the WebAuthn types\n          largeBlob: {\n            write: blob\n          }\n        }\n      }\n    });\n\n    if (credential) {\n      // @ts-ignore -- LargeBlob extension is not yet in the WebAuthn types\n      if (credential.getClientExtensionResults().largeBlob.written) {\n        // Success, the large blob was written.\n        alert('Signer data saved successfully.');\n        onSignerStored(true);\n        return;\n      } else {\n        // The large blob could not be written (e.g. because of a lack of space).\n        // The assertion is still valid.\n        alert('Could not save largeBlob.');\n      }\n    } else {\n      // The user did not complete the challenge.\n      alert('User did not complete the challenge.');\n    }\n  } catch (error) {\n    console.error('Error creating passkey:', error);\n    alert('Failed to get passkey and store signer data.');\n  }\n\n  onSignerStored(true);\n}\n\nexport async function createNewPasskey({\n  user,\n  onPasskeyCreated\n}: {\n  user: UserWithKey;\n  onPasskeyCreated: (created: boolean) => void;\n}) {\n  {\n    const rp = {\n      name: process.env.NEXT_PUBLIC_FC_CLIENT_NAME || 'opencast',\n      id: location.hostname\n    };\n\n    try {\n      const challenge = new Uint8Array(32);\n      await navigator.credentials.create({\n        publicKey: {\n          challenge,\n          rp,\n          user: {\n            displayName: user.name,\n            name: user.username,\n            id: Buffer.from(user.id)\n          },\n          authenticatorSelection: {\n            residentKey: 'required'\n          },\n          pubKeyCredParams: [\n            { alg: -7, type: 'public-key' },\n            { alg: -257, type: 'public-key' }\n          ],\n          extensions: {\n            // @ts-ignore -- LargeBlob extension is not yet in the WebAuthn types\n            largeBlob: { support: 'required' }\n          }\n        }\n      });\n      alert('Credential created, please load the passkey to save your signer.');\n      onPasskeyCreated(true);\n    } catch (error) {\n      console.error('Error creating passkey:', error);\n      alert('Failed to create passkey and store signer data.');\n    }\n  }\n}\n"
  },
  {
    "path": "src/lib/prisma.ts",
    "content": "import 'dotenv/config';\nimport { PrismaClient } from '@prisma/client';\n\nconst globalForPrisma = global as unknown as {\n  prisma: PrismaClient | undefined;\n};\n\nexport const prisma =\n  globalForPrisma.prisma ??\n  new PrismaClient({\n    // log: ['query', 'info', 'warn']\n  });\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n"
  },
  {
    "path": "src/lib/random.ts",
    "content": "const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\nexport function getRandomId(): string {\n  return Array.from({ length: 20 }).reduce(\n    (acc: string) => acc + CHARS[~~(Math.random() * CHARS.length)],\n    ''\n  );\n}\n\nexport function getRandomInt(min: number, max: number): number {\n  return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n"
  },
  {
    "path": "src/lib/signers.ts",
    "content": "import { prisma } from './prisma';\nimport { SignerDetail } from './types/signer';\n\nexport async function getSignerDetail(\n  pubKey: string\n): Promise<SignerDetail | null> {\n  const pubKeyBytes = Buffer.from(pubKey, 'hex');\n\n  const signersRaw = await prisma.$queryRaw<any>`\n    SELECT\n      COUNT(DISTINCT m.hash) AS message_count,\n      MAX(m.timestamp) AS last_message_timestamp,\n      MAX(s.created_at) AS signer_created_at,\n      MAX(s.timestamp) AS signer_timestamp,\n      MAX(s.name) as signer_name,\n      s.signer as pubkey,\n      COUNT(DISTINCT CASE WHEN m.type = 0 THEN m.hash ELSE NULL END) AS none_count,\n      COUNT(DISTINCT CASE WHEN m.type = 1 THEN m.hash ELSE NULL END) AS cast_add_count,\n      COUNT(DISTINCT CASE WHEN m.type = 2 THEN m.hash ELSE NULL END) AS cast_remove_count,\n      COUNT(DISTINCT CASE WHEN m.type = 3 THEN m.hash ELSE NULL END) AS reaction_add_count,\n      COUNT(DISTINCT CASE WHEN m.type = 4 THEN m.hash ELSE NULL END) AS reaction_remove_count,\n      COUNT(DISTINCT CASE WHEN m.type = 5 THEN m.hash ELSE NULL END) AS link_add_count,\n      COUNT(DISTINCT CASE WHEN m.type = 6 THEN m.hash ELSE NULL END) AS link_remove_count,\n      COUNT(DISTINCT CASE WHEN m.type = 7 THEN m.hash ELSE NULL END) AS verification_add_eth_address_count,\n      COUNT(DISTINCT CASE WHEN m.type = 8 THEN m.hash ELSE NULL END) AS verification_remove_count\n    FROM \n      messages m\n    INNER JOIN \n      signers s ON m.signer = s.signer\n    WHERE \n      m.signer = ${pubKeyBytes}\n    GROUP BY\n      s.signer\n  `;\n\n  const [signer] = signersRaw.map((signer: any) => ({\n    pubKey: `0x${signer.pubkey}`,\n    messageCount: signer.message_count,\n    createdAtTimestamp: signer.signer_timestamp || signer.signer_created_at,\n    lastMessageTimestamp: signer.last_message_timestamp,\n    name: signer.signer_name,\n    noneCount: signer.none_count,\n    castAddCount: signer.cast_add_count,\n    castRemoveCount: signer.cast_remove_count,\n    reactionAddCount: signer.reaction_add_count,\n    reactionRemoveCount: signer.reaction_remove_count,\n    linkAddCount: signer.link_add_count,\n    linkRemoveCount: signer.link_remove_count,\n    verificationAddEthAddressCount: signer.verification_add_eth_address_count,\n    verificationRemoveCount: signer.verification_remove_count\n  }));\n\n  if (!signer) return null;\n\n  return signer;\n}\n"
  },
  {
    "path": "src/lib/topics/resolve-topic.ts",
    "content": "import { createPublicClient, http, createClient } from 'viem';\nimport * as chains from 'viem/chains';\nimport { resolveChainIcon } from '../chains/resolve-chain-icon';\nimport { populateEmbed } from '../embeds';\nimport { LRU } from '../lru-cache';\nimport { TopicType } from '../types/topic';\nimport { ExternalEmbed } from '../types/tweet';\nimport { parseChainURL, truncateAddress } from '../utils';\n\nconst chainById = Object.values(chains).reduce(\n  (acc: { [key: string]: chains.Chain }, cur) => {\n    if (cur.id) acc[cur.id] = cur;\n    return acc;\n  },\n  {}\n);\n\nexport type TopicsMapType = { [key: string]: TopicType };\n\nexport type FarcasterChannel = {\n  id: string;\n  url: string;\n  name: string;\n  description: string;\n  imageUrl: string;\n  createdAt: string;\n  followerCount: number;\n  leadFid: number;\n};\n\nexport async function resolveTopic(url: string): Promise<TopicType | null> {\n  const key = `topic:${url}`;\n  const cached = LRU.get(key);\n  if (cached !== undefined) {\n    return cached as TopicType;\n  }\n\n  const resolved = await _resolveTopic(url);\n  // If we can't resolve the topic, cache it for 5 minutes\n  LRU.set(key, resolved, {\n    ttl: resolved === null ? 5 * 60 * 1000 : undefined\n  });\n\n  return resolved;\n}\n\nfunction cleanUrl(url: string): string {\n  if (url.startsWith('https://')) {\n    return cleanUrl(url.slice(8));\n  } else if (url.startsWith('www.')) {\n    return url.slice(4);\n  } else {\n    // TODO: Remove tracking params (utm*) and trailing slash\n    return url;\n  }\n}\n\nasync function getAllChannelsIndexed() {\n  const key = 'all-channels';\n  const cached = LRU.get(key);\n  if (cached !== undefined) {\n    return cached as { [key: string]: FarcasterChannel };\n  }\n\n  const {\n    result: { channels }\n  } = (await fetch('https://api.warpcast.com/v2/all-channels').then((r) =>\n    r.json()\n  )) as { result: { channels: FarcasterChannel[] } };\n\n  const channelsByUrl = channels.reduce((acc, cur) => {\n    acc[cur.url] = cur;\n    return acc;\n  }, {} as { [key: string]: FarcasterChannel });\n\n  LRU.set(key, channelsByUrl, {\n    ttl: 5 * 60 * 1000\n  });\n\n  return channelsByUrl;\n}\n\nasync function getChannel(url: string) {\n  const channelsByUrl = await getAllChannelsIndexed();\n  return channelsByUrl[url];\n}\n\n// CAIP-19 URL\nasync function _resolveTopic(url: string): Promise<TopicType | null> {\n  const farcasterChannel = await getChannel(url);\n\n  if (farcasterChannel) {\n    return {\n      name: farcasterChannel.name,\n      description: farcasterChannel.description,\n      image: farcasterChannel.imageUrl,\n      url\n    };\n  }\n\n  if (url.startsWith('https://')) {\n    let metadata: ExternalEmbed | null;\n    try {\n      metadata = await populateEmbed({ url: url });\n    } catch {\n      return {\n        name: cleanUrl(url),\n        description: 'Link',\n        url\n      };\n    }\n\n    const { text, title, icon } = metadata || {};\n    const cleanedUrl = cleanUrl(url);\n    let name = title || cleanedUrl;\n    if (name.length > 30) {\n      name = cleanedUrl;\n    }\n    return {\n      name,\n      description: text || 'Link',\n      image: icon,\n      url\n    };\n  } else if (url.startsWith('chain://')) {\n    const parsed = parseChainURL(url);\n\n    if (!parsed || parsed.contractType !== 'erc721') {\n      return null;\n    }\n\n    let chainId: number;\n    try {\n      chainId = parseInt(parsed?.chainId);\n    } catch {\n      return null;\n    }\n\n    const rpcUrl = process.env[`CHAIN_RPC_URL_${chainId}`];\n\n    const client = createPublicClient({\n      chain: chainById[chainId],\n      transport: http(rpcUrl)\n    });\n\n    let uri: string;\n    try {\n      uri = (await client.readContract({\n        address: parsed.contractAddress as `0x${string}`,\n        abi: [\n          {\n            inputs: [],\n            name: 'contractURI',\n            outputs: [{ name: '', type: 'string' }],\n            stateMutability: 'view',\n            type: 'function'\n          }\n        ],\n        functionName: 'contractURI'\n      })) as string;\n    } catch (e) {\n      try {\n        uri = (await client.readContract({\n          address: parsed.contractAddress as `0x${string}`,\n          abi: [\n            {\n              inputs: [{ name: 'tokenId', type: 'uint256' }],\n              name: 'tokenURI',\n              outputs: [{ name: '', type: 'string' }],\n              stateMutability: 'view',\n              type: 'function'\n            }\n          ],\n          functionName: 'tokenURI',\n          args: [BigInt(1)]\n        })) as string;\n      } catch {\n        const truncatedAddress = truncateAddress(parsed.contractAddress);\n\n        const chainIcon = await resolveChainIcon(chainId);\n\n        return {\n          name: `${truncatedAddress}`,\n          description: `NFT on ${chainById[chainId].name} at ${parsed.contractAddress}`,\n          image: chainIcon,\n          url\n        };\n      }\n\n      if (!uri) return null;\n    }\n\n    if (uri.startsWith('data:application/json;base64,')) {\n      const jsonString = Buffer.from(uri.split(',')[1], 'base64').toString();\n\n      let json: any;\n      try {\n        json = JSON.parse(jsonString);\n      } catch (e) {\n        return null;\n      }\n\n      let image = json.image as string;\n      if (image) {\n        if (image.startsWith('ipfs://')) {\n          image = `https://ipfs.io/ipfs/${image.slice(7)}`;\n        }\n      }\n\n      return {\n        name: json.name,\n        description: json.description,\n        image: image,\n        url\n      };\n    } else if (uri.startsWith('ipfs://') || uri.startsWith('https://')) {\n      try {\n        let metadata: any;\n        if (uri.startsWith('ipfs://')) {\n          // Resolve IPFS URI\n          const url = `https://ipfs.io/ipfs/${uri.slice(7)}`;\n          const res = await fetch(url);\n          metadata = await res.json();\n        } else if (uri.startsWith('https://')) {\n          // Resolve HTTPS URI\n          const res = await fetch(uri);\n          metadata = await res.json();\n        }\n\n        let image = metadata.image as string;\n        if (image) {\n          if (image.startsWith('ipfs://')) {\n            image = `https://ipfs.io/ipfs/${image.slice(7)}`;\n          }\n        }\n\n        return {\n          name: metadata.name,\n          description: metadata.description,\n          image: image,\n          url\n        };\n      } catch (e) {\n        console.error(e);\n      }\n    }\n\n    return null;\n  } else {\n    return {\n      name: url,\n      description: 'Other',\n      url\n    };\n  }\n}\n\nexport async function resolveTopicsMap(urls: string[]): Promise<TopicsMapType> {\n  const topicOrNulls = await Promise.all(urls.map((url) => resolveTopic(url)));\n  const topics = topicOrNulls.filter(\n    (topicOrNull) => topicOrNull !== null\n  ) as TopicType[];\n  const topicsMap = topics.reduce((acc: TopicsMapType, cur) => {\n    if (cur) {\n      acc[cur.url] = cur;\n    }\n    return acc;\n  }, {});\n  return topicsMap;\n}\n"
  },
  {
    "path": "src/lib/types/app-auth.ts",
    "content": "import { BaseResponse } from './responses';\n\nexport type AppAuthResponse = BaseResponse<AppAuthType>;\n\nexport type AppAuthType = {\n  requestFid: number;\n  requestSigner: string;\n  signature: string;\n  deadline: number;\n};\n"
  },
  {
    "path": "src/lib/types/available.ts",
    "content": "export type AvailablePlace = {\n  name: string;\n  placeType: PlaceType;\n  url: string;\n  parentid: number;\n  country: string;\n  woeid: number;\n  countryCode: null | string;\n};\n\nexport type PlaceType = {\n  code: number;\n  name: 'Country' | 'Supername' | 'Town' | 'Unknown';\n};\n\nexport type AvailablePlaces = AvailablePlace[];\n"
  },
  {
    "path": "src/lib/types/bookmark.ts",
    "content": "import type { Timestamp, FirestoreDataConverter } from 'firebase/firestore';\n\nexport type Bookmark = {\n  id: string;\n  createdAt: Timestamp;\n};\n\nexport const bookmarkConverter: FirestoreDataConverter<Bookmark> = {\n  toFirestore(bookmark) {\n    return { ...bookmark };\n  },\n  fromFirestore(snapshot, options) {\n    const data = snapshot.data(options);\n\n    return { ...data } as Bookmark;\n  }\n};\n"
  },
  {
    "path": "src/lib/types/feed.ts",
    "content": "export type FeedOrderingType = 'latest' | 'top';\n"
  },
  {
    "path": "src/lib/types/file.ts",
    "content": "export type ImageData = {\n  src: string;\n  alt: string;\n};\n\nexport type ImagesPreview = (ImageData & {\n  id: string;\n})[];\n\nexport type ImagePreview = ImageData & { id: string };\nexport type FileWithId = File & { id: string };\n\nexport type FilesWithId = (File & {\n  id: string;\n})[];\n"
  },
  {
    "path": "src/lib/types/keypair.ts",
    "content": "export type KeyPair = {\n  publicKey: `0x${string}`;\n  privateKey: `0x${string}`;\n};\n"
  },
  {
    "path": "src/lib/types/notifications.ts",
    "content": "import { casts } from '@prisma/client';\nimport { BaseResponse } from './responses';\nimport { Tweet } from './tweet';\nimport { User, UsersMapType } from './user';\n\nexport type MessageMetadata = {\n  message_fid: bigint;\n  message_hash: Buffer;\n  message_type: number;\n  message_timestamp: Date;\n};\n\nexport type ReactionQueryResult = casts &\n  MessageMetadata & {\n    reaction_type: number;\n  };\n\nexport type FollowerQueryResult = MessageMetadata;\n\nexport type RepliesQueryResult = casts &\n  MessageMetadata & { parent_fid: bigint };\n\nexport type MentionsQueryResult = casts &\n  MessageMetadata & {\n    parent_fid: bigint | null;\n  };\n\nexport type BasicNotification = {\n  userId: string;\n  timestamp: Date;\n  messageType: number;\n};\n\nexport type BasicReaction = BasicNotification & {\n  targetCastId: string;\n  reactionType: number;\n};\nexport type BasicFollow = BasicNotification;\nexport type BasicReply = BasicNotification & {\n  castId: string;\n  parentUserId: string;\n};\nexport type BasicMention = BasicNotification & { castId: string };\n\ntype NotificationsSummary = {\n  badgeCount: number;\n  lastChecked: string;\n};\n\nexport type NotificationsResponseSummary = BaseResponse<NotificationsSummary>;\n\nexport type AccumulatedReaction = BasicNotification & {\n  castId: string;\n  reactions: BasicReaction[];\n  reactionType: number;\n  userId: string;\n};\n\nexport type AccumulatedFollow = BasicNotification & {\n  follows: BasicFollow[];\n};\n\nexport type NotificationsResponseFull = BaseResponse<\n  NotificationsSummary & {\n    notifications: BasicNotification[];\n    tweetsMap: { [key: string]: Tweet };\n    usersMap: UsersMapType<User>;\n    cursor: number | null;\n  }\n>;\n"
  },
  {
    "path": "src/lib/types/online.ts",
    "content": "import { BaseResponse } from './responses';\nimport { User } from './user';\n\nexport type AppProfile = { pfp?: string; display?: string; username?: string };\n\nexport type OnlineUsersResponse = BaseResponse<{\n  users: {\n    user: User;\n    lastOnline: Date;\n    appFid: string;\n  }[];\n  appProfilesMap: Record<string, AppProfile>;\n}>;\n"
  },
  {
    "path": "src/lib/types/place.ts",
    "content": "export type TrendsReturn = {\n  data: TrendsResponse;\n  status: number;\n};\n\nexport type TrendsResponse = SuccessResponse | ErrorResponse;\n\nexport type SuccessResponse = {\n  trends: FilteredTrends | Trends;\n  location: string;\n};\n\nexport type ErrorResponse = {\n  errors: [\n    {\n      code: number;\n      message: string;\n    }\n  ];\n};\n\nexport type TrendsData = [\n  {\n    trends: Trends;\n    as_of: string;\n    created_at: string;\n    locations: Location;\n  }\n];\n\nexport type Location = [\n  {\n    name: string;\n    woeid: number;\n  }\n];\n\nexport type Trend = {\n  name: string;\n  url: string;\n  promoted_content: null;\n  query: string;\n  tweet_volume: number | null;\n};\n\nexport type Trends = Trend[];\n\nexport type FilteredTrends = (Trend & {\n  tweet_volume: number;\n})[];\n"
  },
  {
    "path": "src/lib/types/responses.ts",
    "content": "export interface BaseResponse<T> {\n  result?: T;\n  message?: string;\n}\n"
  },
  {
    "path": "src/lib/types/signer.ts",
    "content": "import { Message } from '@farcaster/hub-web';\nimport { BaseResponse } from './responses';\n\nexport type SignerDetail = {\n  name: string | null;\n  pubKey: `0x${string}`;\n  messageCount: number;\n  createdAtTimestamp: string;\n  lastMessageTimestamp: string;\n  noneCount: number;\n  castAddCount: number;\n  castRemoveCount: number;\n  reactionAddCount: number;\n  reactionRemoveCount: number;\n  linkAddCount: number;\n  linkRemoveCount: number;\n  verificationAddEthAddressCount: number;\n  verificationRemoveCount: number;\n};\n\nexport type SignersResponse = BaseResponse<SignerDetail[]>;\nexport type SignerResponse = BaseResponse<SignerDetail>;\n\nexport type MessagesArchive = {\n  messages: Message[];\n  signer: SignerDetail;\n};\n\nexport type MessagesArchiveResponse = BaseResponse<MessagesArchive>;\n"
  },
  {
    "path": "src/lib/types/stats.ts",
    "content": "import type { Timestamp, FirestoreDataConverter } from 'firebase/firestore';\n\nexport type Stats = {\n  likes: string[];\n  tweets: string[];\n  updatedAt: Timestamp | null;\n};\n\nexport const statsConverter: FirestoreDataConverter<Stats> = {\n  toFirestore(bookmark) {\n    return { ...bookmark };\n  },\n  fromFirestore(snapshot, options) {\n    const data = snapshot.data(options);\n\n    return { ...data } as Stats;\n  }\n};\n"
  },
  {
    "path": "src/lib/types/theme.ts",
    "content": "export type Theme = 'light' | 'dim' | 'dark';\nexport type Accent = 'blue' | 'yellow' | 'pink' | 'purple' | 'orange' | 'green';\n"
  },
  {
    "path": "src/lib/types/topic.ts",
    "content": "import { BaseResponse } from './responses';\n\nexport type TopicResponse = BaseResponse<TopicType>;\n\nexport type TopicType = {\n  name: string;\n  description: string;\n  image?: string;\n  url: string;\n};\n"
  },
  {
    "path": "src/lib/types/trends.ts",
    "content": "import { TopicType } from './topic';\nimport { BaseResponse } from './responses';\n\nexport type TrendsResponse = BaseResponse<\n  {\n    topic: TopicType | null;\n    volume: number;\n  }[]\n>;\n"
  },
  {
    "path": "src/lib/types/tweet.ts",
    "content": "import { Embed } from '@farcaster/hub-web';\r\nimport { casts } from '@prisma/client';\r\nimport { Frame } from 'frames.js';\r\nimport { TopicsMapType } from '../topics/resolve-topic';\r\nimport { isValidImageExtension } from '../validation';\r\nimport type { ImagesPreview } from './file';\r\nimport { BaseResponse } from './responses';\r\nimport { TopicType } from './topic';\r\nimport type { User, UsersMapType } from './user';\r\n\r\nexport type Mention = {\r\n  userId: string;\r\n  position: number;\r\n  username?: string;\r\n  user?: User;\r\n};\r\n\r\nexport type ExternalEmbed = {\r\n  title?: string;\r\n  text?: string;\r\n  icon?: string;\r\n  image?: string;\r\n  provider?: string;\r\n  url: string;\r\n  contentType?: string;\r\n  frame?: Frame;\r\n};\r\n\r\nexport type Tweet = {\r\n  id: string;\r\n  text: string | null;\r\n  images: ImagesPreview | null;\r\n  embeds: ExternalEmbed[];\r\n  parent: { id: string; username?: string; userId?: string } | null;\r\n  userLikes: string[];\r\n  createdBy: string;\r\n  user: User | null;\r\n  createdAt: Date;\r\n  updatedAt: Date | null;\r\n  deletedAt: Date | null;\r\n  userReplies: number;\r\n  userRetweets: string[];\r\n  mentions: Mention[];\r\n  client: string | null;\r\n  topic: TopicType | null;\r\n  topicUrl: string | null;\r\n  retweet: { username?: string; userId?: string } | null;\r\n  quoteTweets?: Tweet[];\r\n};\r\n\r\nexport type TweetWithUsers = Tweet & { users: UsersMapType<User> };\r\n\r\nexport type TweetResponse = BaseResponse<TweetWithUsers>;\r\nexport interface TweetRepliesResponse\r\n  extends BaseResponse<{\r\n    tweets: Tweet[];\r\n    nextPageCursor: string | null;\r\n    // fid -> User\r\n    users: UsersMapType<User>;\r\n  }> {}\r\n\r\nexport const populateTweetUsers = (\r\n  tweet: Tweet,\r\n  users: UsersMapType<User>\r\n): Tweet => {\r\n  // Look up parent tweet username in users object\r\n  const resolvedParent = tweet.parent;\r\n  if (resolvedParent && !tweet.parent?.username && tweet.parent?.userId) {\r\n    tweet.parent.username = users[tweet.parent.userId]?.username;\r\n  }\r\n\r\n  // Look up mentions in users object\r\n  const resolvedMentions = tweet.mentions.map((mention) => ({\r\n    ...mention,\r\n    username: users[mention.userId]?.username,\r\n    user: users[mention.userId]\r\n  }));\r\n\r\n  // Look up recast username in users object\r\n  const resolvedRetweet = tweet.retweet;\r\n  if (\r\n    resolvedRetweet &&\r\n    !tweet.retweet?.username &&\r\n    tweet.retweet?.userId &&\r\n    users[tweet.retweet.userId]\r\n  ) {\r\n    tweet.retweet.username = users[tweet.retweet.userId]?.username;\r\n  }\r\n\r\n  const resolvedUser = users[tweet.createdBy];\r\n\r\n  return {\r\n    ...tweet,\r\n    mentions: resolvedMentions,\r\n    parent: resolvedParent,\r\n    retweet: resolvedRetweet,\r\n    user: resolvedUser\r\n  };\r\n};\r\n\r\nexport const populateTweetTopic = (\r\n  tweet: Tweet,\r\n  topics: TopicsMapType\r\n): Tweet => {\r\n  if (tweet.topicUrl) {\r\n    const topic = topics[tweet.topicUrl];\r\n    if (topic) {\r\n      return {\r\n        ...tweet,\r\n        topic\r\n      };\r\n    }\r\n  }\r\n  return tweet;\r\n};\r\n\r\nexport const tweetConverter = {\r\n  toTweet(cast: casts & { client?: string }): Tweet {\r\n    // Check if cast.hash is a buffer\r\n    const isBuffer = Buffer.isBuffer(cast.hash);\r\n\r\n    let parent: { id: string; userId?: string } | null = null;\r\n    if (cast.parent_hash) {\r\n      parent = {\r\n        id: cast.parent_hash.toString('hex'),\r\n        userId: cast.parent_fid?.toString()\r\n      };\r\n    }\r\n\r\n    const embeds = cast.embeds as Embed[];\r\n\r\n    const images =\r\n      embeds.length > 0\r\n        ? embeds\r\n            .filter((embed) => embed.url && isValidImageExtension(embed.url))\r\n            .map((embed) => ({\r\n              src: embed.url,\r\n              alt: '',\r\n              id: embed.url\r\n            }))\r\n        : [];\r\n\r\n    const externalEmbeds: ExternalEmbed[] =\r\n      embeds.length > 0\r\n        ? embeds\r\n            .filter((embed) => embed.url && !isValidImageExtension(embed.url))\r\n            .map((embed) => ({\r\n              url: embed.url!\r\n            }))\r\n        : [];\r\n\r\n    const mentions = (cast.mentions as number[])?.map(\r\n      (userId, index): Mention => ({\r\n        userId: userId.toString(),\r\n        position: (cast.mentions_positions as number[])[index]\r\n      })\r\n    );\r\n\r\n    return {\r\n      id: isBuffer\r\n        ? cast.hash.toString('hex')\r\n        : Buffer.from((cast.hash as any).data).toString('hex'),\r\n      text: cast.text,\r\n      images: images.length > 0 ? images : null,\r\n      embeds: externalEmbeds,\r\n      parent,\r\n      topic: null,\r\n      topicUrl: cast.parent_url,\r\n      userLikes: [],\r\n      createdBy: cast.fid.toString(),\r\n      user: null,\r\n      createdAt: cast.timestamp,\r\n      updatedAt: null,\r\n      deletedAt: cast.deleted_at,\r\n      userReplies: 0,\r\n      userRetweets: [],\r\n      mentions,\r\n      client: cast.client || null,\r\n      retweet: null\r\n    } as Tweet;\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/lib/types/user.ts",
    "content": "import { UserDataType } from '@farcaster/hub-web';\nimport { BaseResponse } from './responses';\nimport type { Accent, Theme } from './theme';\nimport { TopicType } from './topic';\n\nexport type User = {\n  id: string;\n  bio: string | null;\n  name: string;\n  username: string;\n  photoURL: string;\n  verified: boolean;\n};\n\nexport type UserFull = User & {\n  theme: Theme | null;\n  accent: Accent | null;\n  website: string | null;\n  location: string | null;\n  following: string[];\n  followers: string[];\n  createdAt: Date;\n  updatedAt: Date | null;\n  totalTweets: number;\n  totalPhotos: number;\n  pinnedTweet: string | null;\n  coverPhotoURL: string | null;\n  interests: TopicType[];\n  address: string | null;\n};\n\nexport type EditableData = Extract<\n  keyof UserFull,\n  'bio' | 'name' | 'website' | 'photoURL' | 'location' | 'coverPhotoURL'\n>;\n\nexport type EditableUserData = Pick<UserFull, EditableData>;\n\nexport type UserResponse = BaseResponse<UserFull | User>;\n\nexport type UserFullResponse = BaseResponse<UserFull>;\n\nexport type UsersMapType<T> = { [key: string]: T };\n\nexport type KnownFollowersResponse = BaseResponse<{\n  knownFollowerCount: number;\n  resolvedUsers: User[];\n}>;\n\nexport const userConverter = {\n  toUser(user: any): User {\n    return {\n      id: user.fid.toString(),\n      bio: user[UserDataType.BIO] ?? null,\n      name: user[UserDataType.DISPLAY] || '',\n      username: user[UserDataType.USERNAME] || '',\n      photoURL: user[UserDataType.PFP], //user['1'],\n      verified: false\n    } as User;\n  },\n\n  toUserFull(user: any): UserFull {\n    return {\n      id: user.fid.toString(),\n      bio: user[UserDataType.BIO] ?? null,\n      name: user[UserDataType.DISPLAY] || '',\n      theme: null,\n      accent: null,\n      website: null,\n      location: null,\n      username: user[UserDataType.USERNAME] || '',\n      photoURL: user[UserDataType.PFP] || '', //user['1'],\n      coverPhotoURL: null,\n      verified: false,\n      following: [],\n      followers: [],\n      createdAt: new Date(),\n      updatedAt: null,\n      totalTweets: 0,\n      totalPhotos: 0,\n      pinnedTweet: null,\n      interests: [],\n      address: null\n    } as UserFull;\n  }\n};\n"
  },
  {
    "path": "src/lib/user/resolve-user.ts",
    "content": "import { getHubRpcClient, UserDataType } from '@farcaster/hub-web';\nimport { prisma } from '../prisma';\nimport { User, userConverter, UserFull, UsersMapType } from '../types/user';\nimport { TopicType } from '../types/topic';\nimport { resolveTopic } from '../topics/resolve-topic';\nimport { bytesToHex } from 'viem';\nimport { fetchJSON } from '../fetch';\nimport {\n  getInsecureHubRpcClient,\n  getSSLHubRpcClient,\n  isUserDataAddMessage\n} from '@farcaster/hub-nodejs';\n\nasync function getUserDataMap(fid: bigint): Promise<\n  | {\n      type: number;\n      value: string;\n    }[]\n  | null\n> {\n  let userData = (\n    await prisma.user_data.findMany({\n      where: {\n        fid: fid\n      }\n    })\n  ).map((ud) => ({\n    type: ud.type,\n    value: ud.value\n  }));\n\n  if (userData.length === 0) {\n    // Query hub\n    const hubClient =\n      process.env.FC_HUB_USE_TLS === 'true'\n        ? getSSLHubRpcClient(process.env.FC_HUB_URL!)\n        : getInsecureHubRpcClient(process.env.FC_HUB_URL!);\n    const userDataHubResponse = await hubClient.getUserDataByFid({\n      fid: Number(fid)\n    });\n\n    if (!userDataHubResponse.isOk()) {\n      return null;\n    }\n\n    const userDataHub = userDataHubResponse.value.messages;\n\n    userData = userDataHub\n      .filter(isUserDataAddMessage)\n      .map((m) => m.data.userDataBody);\n  }\n\n  const userDataRaw = userData.reduce((acc: any, cur) => {\n    acc = {\n      ...acc,\n      [cur.type]: cur.value\n    };\n    return acc;\n  }, {});\n\n  return userDataRaw;\n}\n\nexport async function resolveUserFromFid(fid: bigint): Promise<User | null> {\n  const userDataRaw = await getUserDataMap(fid);\n\n  const user = userConverter.toUser({ ...userDataRaw, fid });\n\n  return user;\n}\n\nexport async function resolveUserFullFromFid(\n  fid: bigint\n): Promise<UserFull | null> {\n  const userDataRaw = await getUserDataMap(fid);\n\n  const followers = await prisma.links.findMany({\n    where: {\n      target_fid: fid,\n      type: 'follow',\n      deleted_at: null\n    },\n    distinct: ['target_fid', 'fid']\n  });\n\n  const following = await prisma.links.findMany({\n    where: {\n      fid: fid,\n      type: 'follow',\n      deleted_at: null\n    },\n    distinct: ['target_fid', 'fid']\n  });\n\n  const castCount = await prisma.casts.aggregate({\n    where: {\n      fid: fid,\n      deleted_at: null\n    },\n    _count: true\n  });\n\n  const interests = await userInterests(fid);\n\n  const verification = await prisma.verifications.findFirst({\n    where: {\n      fid: fid,\n      deleted_at: null\n    }\n  });\n  const signerAddressBuffer = verification?.signer_address;\n  const userAddress = signerAddressBuffer\n    ? bytesToHex(signerAddressBuffer)\n    : null;\n\n  const user = userConverter.toUserFull({ ...userDataRaw, fid });\n\n  return {\n    ...user,\n    followers: followers.map((f) => f.fid!.toString()),\n    following: following.map((f) => f.target_fid!.toString()),\n    totalTweets: castCount._count,\n    address: userAddress,\n    interests\n  };\n}\n\nexport async function resolveUserAmbiguous(\n  idOrUsername: string,\n  full: boolean = false\n): Promise<User | UserFull | null> {\n  let fid = Number(idOrUsername);\n  if (isNaN(fid)) {\n    const username = (idOrUsername as string).toLowerCase();\n    const usernameData = await prisma.user_data.findFirst({\n      where: {\n        type: UserDataType.USERNAME,\n        value: username\n      }\n    });\n    if (usernameData) {\n      fid = Number(usernameData.fid);\n    } else {\n      return null;\n    }\n  }\n\n  if (full) {\n    return await resolveUserFullFromFid(BigInt(fid));\n  } else {\n    return await resolveUserFromFid(BigInt(fid));\n  }\n}\n\n// TODO: Combine into one query\nexport async function resolveUsers(\n  fids: bigint[],\n  full: boolean = false\n): Promise<(User | UserFull)[]> {\n  const users = await Promise.all(\n    !full\n      ? fids.map((fid) => resolveUserFromFid(fid))\n      : fids.map((fid) => resolveUserFullFromFid(fid))\n  );\n  return users.filter((user) => user !== null) as (User | UserFull)[];\n}\n\n// TODO: Combine into one query\n// TODO: Cache results\nexport async function resolveUsersMap(\n  fids: bigint[],\n  full: boolean = false\n): Promise<UsersMapType<User | UserFull>> {\n  const userOrNulls = await Promise.all(\n    fids.map((fid) => resolveUserFromFid(fid))\n  );\n  const users = userOrNulls.filter(\n    (userOrNull) => userOrNull !== null\n  ) as User[];\n  const usersMap = users.reduce((acc: UsersMapType<User | UserFull>, cur) => {\n    if (cur) {\n      acc[cur.id] = cur;\n    }\n    return acc;\n  }, {});\n  return usersMap;\n}\n\nexport async function userInterests(fid: bigint): Promise<TopicType[]> {\n  const reactionGroups = (await prisma.$queryRaw`\n        SELECT \n            c.parent_url, \n            COUNT(*) as reaction_count \n        FROM \n            reactions r\n        INNER JOIN \n            casts c ON r.target_cast_hash = c.hash \n        WHERE \n            r.fid = ${fid}  \n            AND c.deleted_at IS NULL \n            AND c.parent_url IS NOT NULL \n        GROUP BY c.parent_url\n        ORDER BY reaction_count DESC\n        LIMIT 5;\n      `) as { parent_url: string; reaction_count: number }[];\n\n  const topics = (\n    await Promise.all(\n      reactionGroups.map(async (group) => {\n        const url = group.parent_url!;\n        const topic = await resolveTopic(url);\n        if (!topic) {\n          console.error(`Unresolved topic: ${group.parent_url}`);\n        }\n        return topic;\n      })\n    )\n  ).filter((topic) => topic !== null) as TopicType[];\n\n  return topics;\n}\n"
  },
  {
    "path": "src/lib/utils.ts",
    "content": "import type { SyntheticEvent } from 'react';\nimport type { MotionProps } from 'framer-motion';\nimport isURL from 'validator/lib/isURL';\n\nexport function preventBubbling(\n  callback?: ((...args: never[]) => unknown) | null,\n  noPreventDefault?: boolean\n) {\n  return (e: SyntheticEvent): void => {\n    e.stopPropagation();\n\n    if (!noPreventDefault) e.preventDefault();\n    if (callback) callback();\n  };\n}\n\nexport function hasAncestorWithClass(element: HTMLElement, className: string) {\n  let currentElement: HTMLElement | null = element;\n  while (currentElement) {\n    if (\n      currentElement.classList &&\n      currentElement.classList.contains(className)\n    ) {\n      return true;\n    }\n    currentElement = currentElement.parentElement;\n  }\n  return false;\n}\n\nexport function delayScroll(ms: number) {\n  return (): NodeJS.Timeout => setTimeout(() => window.scrollTo(0, 0), ms);\n}\n\nexport function sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport function getStatsMove(movePixels: number): MotionProps {\n  return {\n    initial: {\n      opacity: 0,\n      y: -movePixels\n    },\n    animate: {\n      opacity: 1,\n      y: 0\n    },\n    exit: {\n      opacity: 0,\n      y: movePixels\n    },\n    transition: {\n      type: 'tween',\n      duration: 0.15\n    }\n  };\n}\n\nexport function isPlural(count: number): string {\n  return count > 1 ? 's' : '';\n}\n\nexport function replaceOccurrencesMultiple(\n  text: string,\n  occurrences: string[],\n  replacement: string\n): string {\n  return occurrences.reduce(\n    (acc, occurrence) => acc.replace(occurrence, replacement),\n    text\n  );\n}\n\nexport type ParsedChainURL = {\n  scheme: string;\n  chainId: string;\n  contractType: string;\n  contractAddress: string;\n};\n\nexport function parseChainURL(url: string): ParsedChainURL | null {\n  const matches = url.match(\n    /^(chain:\\/\\/)([\\w\\d]+):([\\w\\d]+)\\/([\\w\\d]+):([\\w\\d]+)$/\n  );\n\n  if (!matches) {\n    return null;\n  }\n\n  const [_, scheme, , chainId, contractType, contractAddress] = matches;\n\n  return {\n    scheme,\n    chainId,\n    contractType,\n    contractAddress\n  };\n}\n\nexport function getHttpsUrls(text: string): string[] {\n  const words = text\n    .split(' ')\n    .map((word) => word.split('\\n'))\n    .flat();\n  const urls = words.filter((word) =>\n    isURL(word, { require_tld: true, require_protocol: false })\n  );\n  // Add https to urls that don't have a protocol\n  const httpsUrls = urls.map((url) => {\n    if (url.startsWith('http://')) {\n      return url.replace('http://', 'https://');\n    } else if (!url.startsWith('https://')) {\n      return `https://${url}`;\n    }\n    return url;\n  });\n  const uniqueUrls = [...new Set(httpsUrls)];\n  return uniqueUrls;\n}\n\nexport const truncateAddress = (address: string): string => {\n  return `${address.slice(0, 6)}...${address.slice(-4)}`;\n};\n\nconst replacer = (key: any, value: any) => {\n  if (\n    value !== null &&\n    typeof value === 'object' &&\n    'type' in value &&\n    value.type === 'Buffer' &&\n    'data' in value\n  ) {\n    // Convert Buffer to a hex string\n    return Buffer.from(value).toString('hex');\n  } else if (typeof value === 'bigint') {\n    // Convert bigint to string\n    return value.toString();\n  }\n  return value;\n};\n\nexport function JSONStringify<T>(data: T): string {\n  return JSON.stringify(data, replacer);\n}\n\nexport function JSONParse<T>(data: string): T {\n  return JSON.parse(data);\n}\n\nexport function serialize<T>(data: T): T {\n  return JSONParse(JSONStringify(data));\n}\n"
  },
  {
    "path": "src/lib/validation.ts",
    "content": "import { getRandomId } from './random';\nimport type { FilesWithId, FileWithId, ImagesPreview } from './types/file';\n\nconst IMAGE_EXTENSIONS = [\n  'apng',\n  'avif',\n  'gif',\n  'jpg',\n  'jpeg',\n  'jfif',\n  'pjpeg',\n  'pjp',\n  'png',\n  'svg',\n  'webp'\n] as const;\n\ntype ImageExtensions = (typeof IMAGE_EXTENSIONS)[number];\n\nexport function isValidImageExtension(\n  extension: string\n): extension is ImageExtensions {\n  return IMAGE_EXTENSIONS.includes(\n    extension.split('.').pop()?.toLowerCase() as ImageExtensions\n  );\n}\n\nexport function isValidImage(name: string, bytes: number): boolean {\n  return isValidImageExtension(name) && bytes < 20 * Math.pow(1024, 2);\n}\n\nexport function isValidUsername(\n  username: string,\n  value: string\n): string | null {\n  if (value.length < 4)\n    return 'Your username must be longer than 4 characters.';\n  if (value.length > 15)\n    return 'Your username must be shorter than 15 characters.';\n  if (!/^\\w+$/i.test(value))\n    return \"Your username can only contain letters, numbers and '_'.\";\n  if (!/[a-z]/i.test(value)) return 'Include a non-number character.';\n  if (value === username) return 'This is your current username.';\n\n  return null;\n}\n\ntype ImagesData = {\n  imagesPreviewData: ImagesPreview;\n  selectedImagesData: FilesWithId;\n};\n\nexport function getImagesData(\n  files: FileList | null,\n  currentFiles?: number\n): ImagesData | null {\n  if (!files || !files.length) return null;\n\n  const singleEditingMode = currentFiles === undefined;\n\n  const rawImages =\n    singleEditingMode ||\n    !(currentFiles === 4 || files.length > 4 - currentFiles)\n      ? Array.from(files).filter(({ name, size }) => isValidImage(name, size))\n      : null;\n\n  if (!rawImages || !rawImages.length) return null;\n\n  const imagesId = rawImages.map(({ name }) => {\n    const randomId = getRandomId();\n    return {\n      id: randomId,\n      name: name === 'image.png' ? `${randomId}.png` : null\n    };\n  });\n\n  const imagesPreviewData = rawImages.map((image, index) => ({\n    id: imagesId[index].id,\n    src: URL.createObjectURL(image),\n    alt: imagesId[index].name ?? image.name\n  }));\n\n  const selectedImagesData = rawImages.map((image, index) =>\n    renameFile(image, imagesId[index].id, imagesId[index].name)\n  );\n\n  return { imagesPreviewData, selectedImagesData };\n}\n\nfunction renameFile(\n  file: File,\n  newId: string,\n  newName: string | null\n): FileWithId {\n  return Object.assign(\n    newName\n      ? new File([file], newName, {\n          type: file.type,\n          lastModified: file.lastModified\n        })\n      : file,\n    { id: newId }\n  );\n}\n"
  },
  {
    "path": "src/pages/404.tsx",
    "content": "import Error from 'next/error';\nimport { useTheme } from '@lib/context/theme-context';\nimport { SEO } from '@components/common/seo';\n\nexport default function NotFound(): JSX.Element {\n  const { theme } = useTheme();\n\n  const isDarkMode = ['dim', 'dark'].includes(theme);\n\n  return (\n    <>\n      <SEO\n        title='Page not found / Opencast'\n        description='Sorry we couldn’t find the page you were looking for.'\n        image='/404.png'\n      />\n      <Error statusCode={404} withDarkMode={isDarkMode} />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/pages/[...redirect].tsx",
    "content": "import NotFound from './404';\n\nexport default function Redirect(): JSX.Element {\n  return <NotFound />;\n}\n"
  },
  {
    "path": "src/pages/_app.tsx",
    "content": "import '@rainbow-me/rainbowkit/styles.css';\nimport '@styles/globals.scss';\n\nimport { AppHead } from '@components/common/app-head';\nimport { AuthContextProvider } from '@lib/context/auth-context';\nimport { ThemeContextProvider } from '@lib/context/theme-context';\nimport { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';\nimport type { NextPage } from 'next';\nimport type { AppProps } from 'next/app';\nimport type { ReactElement, ReactNode } from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { WagmiProvider } from 'wagmi';\nimport { arbitrum, base, mainnet, optimism, polygon, zora } from 'wagmi/chains';\n\nconst queryClient = new QueryClient();\n\ntype NextPageWithLayout = NextPage & {\n  getLayout?: (page: ReactElement) => ReactNode;\n};\n\ntype AppPropsWithLayout = AppProps & {\n  Component: NextPageWithLayout;\n};\n\nconst config = getDefaultConfig({\n  appName: 'Opencast',\n  projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_ID!,\n  chains: [arbitrum, base, mainnet, optimism, polygon, zora]\n});\n\n// const wagmiConfig = createConfig({\n//   autoConnect: true,\n//   connectors,\n//   publicClient\n// });\n\nexport default function App({\n  Component,\n  pageProps\n}: AppPropsWithLayout): ReactNode {\n  const getLayout = Component.getLayout ?? ((page): ReactNode => page);\n\n  return (\n    <>\n      <AppHead />\n      <WagmiProvider config={config}>\n        <QueryClientProvider client={queryClient}>\n          <RainbowKitProvider>\n            <AuthContextProvider>\n              <ThemeContextProvider>\n                {getLayout(<Component {...pageProps} />)}\n              </ThemeContextProvider>\n            </AuthContextProvider>\n          </RainbowKitProvider>\n        </QueryClientProvider>\n      </WagmiProvider>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/pages/_document.tsx",
    "content": "import { Html, Head, Main, NextScript } from 'next/document';\n\nexport default function Document(): JSX.Element {\n  return (\n    <Html lang='en'>\n      <Head />\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n"
  },
  {
    "path": "src/pages/api/embeds.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\r\nimport { populateEmbed } from '../../lib/embeds';\r\nimport { ExternalEmbed } from '../../lib/types/tweet';\r\n\r\nexport default async function handle(\r\n  req: NextApiRequest,\r\n  res: NextApiResponse<(ExternalEmbed | null)[]>\r\n) {\r\n  const { method } = req;\r\n  switch (method) {\r\n    case 'GET':\r\n      let urls = (req.query.urls as string).split(',');\r\n\r\n      const embeds = await Promise.all(\r\n        urls.map((url) => populateEmbed({ url }))\r\n      );\r\n\r\n      res.json(embeds);\r\n      break;\r\n    default:\r\n      res.setHeader('Allow', ['GET']);\r\n      res.status(405).end(`Method ${method} Not Allowed`);\r\n  }\r\n}\r\n"
  },
  {
    "path": "src/pages/api/feed.ts",
    "content": "import { Prisma, casts } from '@prisma/client';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport {\n  PaginatedTweetsResponse,\n  PaginatedTweetsType,\n  TweetsResponse,\n  getTweetsPaginatedRawSql\n} from '../../lib/paginated-tweets';\nimport { prisma } from '../../lib/prisma';\nimport { FeedOrderingType } from '../../lib/types/feed';\nimport { tweetConverter } from '../../lib/types/tweet';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<PaginatedTweetsResponse | TweetsResponse | string>\n) {\n  if (req.method === 'OPTIONS') {\n    return res.status(200).send('Ok');\n  }\n\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const userFid = req.query.fid ? Number(req.query.fid) : null;\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : null;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      const skip = req.query.skip ? Number(req.query.skip) : 0;\n      const after = !!req.query.after && req.query.after !== 'false';\n      const full = !!req.query.full && req.query.full !== 'false';\n      const ordering: FeedOrderingType = req.query.ordering\n        ? (req.query.ordering as FeedOrderingType)\n        : 'latest';\n      const topicUrl = req.query.topic_url\n        ? decodeURIComponent(req.query.topic_url as string)\n        : null;\n\n      // Get all the target_fids (people that the user follows)\n      let targetFids: bigint[] | null = null;\n      if (userFid != null) {\n        const links = await prisma.links.findMany({\n          where: {\n            fid: userFid,\n            deleted_at: null\n          },\n          select: {\n            target_fid: true\n          }\n        });\n        targetFids = [\n          ...(links.map((link) => link.target_fid) as bigint[]),\n          BigInt(userFid)\n        ];\n      }\n\n      let reverseChronologicalQuery = Prisma.sql``;\n      let topQuery = Prisma.sql``;\n\n      // Build queries for top and reverse chronological and handle edge case where targetFids is empty\n      // TODO: Find a cleaner way to conditionally add where clauses\n      if (targetFids !== null && targetFids.length !== 0) {\n        reverseChronologicalQuery = Prisma.sql`\n          SELECT * FROM casts\n          WHERE\n            fid IN (${targetFids ? Prisma.join(targetFids) : 'null::bigint'})\n            AND (\n              ${cursor} IS NULL\n              OR \n              (${!after} IS TRUE AND timestamp < ${cursor}::timestamp)\n              OR\n              (${after} IS TRUE AND timestamp > ${cursor}::timestamp)\n            )\n            AND (\n              ${topicUrl}::text IS NULL\n              OR\n              (${topicUrl}::text IS NOT NULL AND parent_url = ${topicUrl}::text)\n            )\n            AND parent_hash IS NULL\n            AND casts.deleted_at IS NULL\n          ORDER BY timestamp DESC\n          LIMIT ${limit}\n          OFFSET ${skip}\n        `;\n\n        topQuery = Prisma.sql`\n          SELECT \n              casts.*,\n              COUNT(reactions.id) AS like_count\n          FROM \n              casts\n          LEFT JOIN \n              reactions ON casts.hash = reactions.target_cast_hash AND reactions.type = 1\n          WHERE \n            casts.parent_hash is null  \n            AND casts.deleted_at is null\n            AND casts.fid IN (${\n              targetFids ? Prisma.join(targetFids) : 'null::bigint'\n            })\n            AND casts.timestamp > ${new Date(\n              (cursor || new Date()).getTime() - 1000 * 60 * 60 * 24\n            )}\n            AND (\n              ${topicUrl}::text IS NULL\n              OR\n              (${topicUrl}::text IS NOT NULL AND parent_url = ${topicUrl}::text)\n            )\n          GROUP BY \n              casts.id\n          ORDER BY \n              like_count DESC\n          LIMIT ${limit}\n          OFFSET ${skip}\n        `;\n      } else {\n        reverseChronologicalQuery = Prisma.sql`\n          SELECT * FROM casts\n          WHERE\n            (\n              ${cursor}::timestamp IS NULL\n              OR \n              (${!after} IS TRUE AND timestamp < ${cursor}::timestamp)\n              OR\n              (${after} IS TRUE AND timestamp > ${cursor}::timestamp)\n            )\n            AND (\n              ${topicUrl}::text IS NULL\n              OR\n              (${topicUrl}::text IS NOT NULL AND parent_url = ${topicUrl}::text)\n            )\n            AND parent_hash IS NULL\n            AND casts.deleted_at IS NULL\n          ORDER BY timestamp DESC\n          LIMIT ${limit}\n          OFFSET ${skip}\n        `;\n\n        topQuery = Prisma.sql`\n          SELECT \n              casts.*,\n              COUNT(reactions.id) AS like_count\n          FROM \n              casts\n          LEFT JOIN \n              reactions ON casts.hash = reactions.target_cast_hash AND reactions.type = 1\n          WHERE \n            casts.parent_hash is null  \n            AND casts.deleted_at is null\n            AND casts.timestamp > ${new Date(\n              (cursor || new Date()).getTime() - 1000 * 60 * 60 * 24\n            )}\n            AND (\n              ${topicUrl}::text IS NULL\n              OR\n              (${topicUrl}::text IS NOT NULL AND parent_url = ${topicUrl}::text)\n            )\n          GROUP BY \n              casts.id\n          ORDER BY \n              like_count DESC\n          LIMIT ${limit}\n          OFFSET ${skip}\n        `;\n      }\n\n      if (!full) {\n        const casts = await prisma.$queryRaw<casts[]>(\n          reverseChronologicalQuery\n        );\n        const tweets = casts.map(tweetConverter.toTweet);\n        res.json({\n          result: { tweets }\n        });\n        return;\n      }\n\n      let result: PaginatedTweetsType;\n\n      result = await getTweetsPaginatedRawSql(\n        ordering === 'top' ? topQuery : reverseChronologicalQuery,\n        skip !== undefined\n          ? () => {\n              return (skip + limit).toString();\n            }\n          : undefined\n      );\n\n      res.json({\n        result\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/hub/batch.ts",
    "content": "import { Message } from '@farcaster/hub-nodejs';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport { hubClient } from '../../../lib/farcaster';\nimport { BaseResponse } from '../../../lib/types/responses';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<BaseResponse<{}>>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'POST':\n      const messagesRaw = req.body.messages;\n      const messages: Message[] = messagesRaw.map((m: any) =>\n        Message.fromJSON(m)\n      );\n\n      try {\n        await Promise.all(\n          messages.map(async (m) => {\n            const hubResult = await hubClient.submitMessage(m);\n            return hubResult.unwrapOr(null);\n          })\n        );\n      } catch (error) {\n        console.log(error);\n        res.status(400).json({ message: 'Could not send message' });\n        return;\n      }\n\n      res.json({ message: 'Success' });\n\n      break;\n\n    default:\n      res.setHeader('Allow', ['POST']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/hub/index.ts",
    "content": "import { Message } from '@farcaster/hub-nodejs';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport { hubClient } from '../../../lib/farcaster';\nimport { BaseResponse } from '../../../lib/types/responses';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<BaseResponse<Message>>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'POST':\n      const message = Message.fromJSON(req.body.message);\n      const hubResult = await hubClient.submitMessage(message);\n      const unwrapped = hubResult.unwrapOr(null);\n      if (!unwrapped) {\n        res.status(400).json({ message: 'Could not send message' });\n        return;\n      }\n      res.json({ result: Message.toJSON(unwrapped) as Message });\n      break;\n    case 'GET':\n      // Get cast\n      const hash = req.query.hash as string;\n      const fid = parseInt(req.query.fid as string);\n      const castResult = await hubClient.getCast({\n        fid,\n        hash: Buffer.from(hash, 'hex')\n      });\n      const cast = castResult.unwrapOr(null);\n      if (!cast) {\n        res.status(400).json({ message: 'Could not get cast' });\n        return;\n      }\n      res.json({ result: Message.toJSON(cast) as Message });\n      break;\n\n    default:\n      res.setHeader('Allow', ['POST']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/online/index.ts",
    "content": "import { Prisma } from '@prisma/client';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport { prisma } from '../../../lib/prisma';\nimport { AppProfile, OnlineUsersResponse } from '../../../lib/types/online';\nimport { UserFull } from '../../../lib/types/user';\nimport { resolveUserFromFid } from '../../../lib/user/resolve-user';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<OnlineUsersResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      const fid = req.query.fid as string;\n\n      const cutoffTime = new Date(new Date().getTime() - 1000 * 60 * 60 * 2); // 2 hours ago\n\n      // Get last cast/reaction for all user's following\n      const following = await prisma.links.findMany({\n        where: {\n          fid: BigInt(fid),\n          type: 'follow',\n          deleted_at: null\n        },\n        distinct: ['target_fid', 'fid']\n      });\n\n      const fids = following.map((f) => f.target_fid);\n\n      const casts = await prisma.casts.findMany({\n        where: {\n          fid: {\n            in: fids\n          },\n          timestamp: {\n            gte: cutoffTime\n          }\n        },\n        orderBy: {\n          timestamp: 'desc'\n        },\n        take: limit,\n        distinct: ['fid']\n      });\n\n      const reactions = await prisma.reactions.findMany({\n        where: {\n          fid: {\n            in: fids\n          },\n          timestamp: {\n            gte: cutoffTime\n          }\n        },\n        orderBy: {\n          timestamp: 'desc'\n        },\n        take: limit,\n        distinct: ['fid']\n      });\n\n      const combined = [...casts, ...reactions];\n\n      const signersSet = new Set(combined.map((c) => c.signer));\n\n      const timestampsByUser = combined\n        .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()) // ascending order - oldest first\n        .reduce((acc, cur) => {\n          if (!acc[cur.fid.toString()]) {\n            acc[cur.fid.toString()] = {\n              lastOnline: cur.timestamp,\n              fid: cur.fid,\n              signer: cur.signer\n            };\n          }\n          return acc;\n        }, {} as Record<string, { lastOnline: Date; fid: bigint; signer: Buffer }>);\n\n      type SignerRow = {\n        user_fid: bigint;\n        requester_fid: bigint;\n        pfp?: string;\n        display?: string;\n        bio?: string;\n        url?: string;\n        username?: string;\n      };\n\n      // TODO: Update this with convenience view\n      const signers = (await prisma.$queryRaw`\n        SELECT DISTINCT \n          s.fid as user_fid,\n          s.key,\n          s.requester_fid,\n          MAX(CASE WHEN ud.type = 1 THEN ud.value END) AS pfp,\n          MAX(CASE WHEN ud.type = 2 THEN ud.value END) AS display,\n          MAX(CASE WHEN ud.type = 3 THEN ud.value END) AS bio,\n          MAX(CASE WHEN ud.type = 5 THEN ud.value END) AS url,\n          MAX(CASE WHEN ud.type = 6 THEN ud.value END) AS username\n        FROM signers s\n        LEFT JOIN user_data ud ON s.requester_fid = ud.fid\n        WHERE s.key IN (${Prisma.join(Array.from(signersSet))})\n        GROUP BY s.requester_fid, s.key, s.fid\n      `) as SignerRow[];\n\n      const appProfilesMap = signers.reduce((acc, cur) => {\n        acc[cur.requester_fid.toString()] = {\n          display: cur.display,\n          pfp: cur.pfp,\n          username: cur.username\n        };\n        return acc;\n      }, {} as Record<string, AppProfile>);\n\n      const appFidsByUserFid = signers.reduce((acc, cur) => {\n        acc[cur.user_fid.toString()] = cur.requester_fid.toString();\n        return acc;\n      }, {} as Record<string, string>);\n\n      const users = (\n        (\n          await Promise.all(\n            Object.values(timestampsByUser).map((user) =>\n              resolveUserFromFid(user.fid)\n            )\n          )\n        ).filter((user) => user !== null) as UserFull[]\n      )\n        .sort((a, b) => {\n          return (\n            timestampsByUser[b!.id.toString()].lastOnline.getTime() -\n            timestampsByUser[a!.id.toString()].lastOnline.getTime()\n          );\n        })\n        .map((u) => {\n          return {\n            user: u,\n            lastOnline: timestampsByUser[u!.id.toString()].lastOnline,\n            appFid: appFidsByUserFid[u!.id.toString()]\n          };\n        });\n\n      res.json({ result: { users, appProfilesMap } });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/search.ts",
    "content": "import { UserDataType } from '@farcaster/hub-web';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport { prisma } from '../../lib/prisma';\nimport { BaseResponse } from '../../lib/types/responses';\nimport { User } from '../../lib/types/user';\nimport { resolveUserFromFid } from '../../lib/user/resolve-user';\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<BaseResponse<User[]>>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const query = req.query.q as string;\n\n      if (!query.length) {\n        res.json({ result: [] });\n        return;\n      }\n\n      // Get users that match query\n      const userData = await prisma.user_data.findMany({\n        where: {\n          type: {\n            in: [UserDataType.USERNAME, UserDataType.DISPLAY]\n          },\n          deleted_at: null,\n          value: {\n            contains: query,\n            mode: 'insensitive'\n          }\n        },\n        take: 5,\n        distinct: 'fid'\n      });\n\n      const fids = userData.map((userData) => userData.fid);\n\n      const userOrNulls = await Promise.all(\n        fids.map((fid) => resolveUserFromFid(fid))\n      );\n      const users = userOrNulls.filter(\n        (userOrNull) => userOrNull !== null\n      ) as User[];\n\n      res.json({ result: users });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/signer/[pubKey]/authorize.ts",
    "content": "import type { NextApiRequest, NextApiResponse } from 'next';\nimport { mnemonicToAccount } from 'viem/accounts';\nimport { AppAuthResponse } from '../../../../lib/types/app-auth';\n\n// https://warpcast.notion.site/Signer-Request-API-Migration-Guide-Public-9e74827f9070442fb6f2a7ffe7226b3c\n\ntype SignerEndpointQuery = {\n  pubKey: `0x${string}`;\n};\n\nconst SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {\n  name: 'Farcaster SignedKeyRequestValidator',\n  version: '1',\n  chainId: 10,\n  verifyingContract: '0x00000000fc700472606ed4fa22623acf62c60553'\n} as const;\n\nconst SIGNED_KEY_REQUEST_TYPE = [\n  { name: 'requestFid', type: 'uint256' },\n  { name: 'key', type: 'bytes' },\n  { name: 'deadline', type: 'uint256' }\n] as const;\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<AppAuthResponse>\n): Promise<void> {\n  const { pubKey } = req.query as SignerEndpointQuery;\n\n  const appFid = process.env.APP_FID!;\n  const account = mnemonicToAccount(process.env.APP_MNENOMIC!);\n\n  const deadline = Math.floor(Date.now() / 1000) + 86400; // signature is valid for 1 day\n  const signature = await account.signTypedData({\n    domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,\n    types: {\n      SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE\n    },\n    primaryType: 'SignedKeyRequest',\n    message: {\n      requestFid: BigInt(appFid),\n      key: pubKey,\n      deadline: BigInt(deadline)\n    }\n  });\n\n  res.json({\n    result: {\n      signature,\n      requestFid: parseInt(appFid),\n      deadline,\n      requestSigner: account.address\n    }\n  });\n}\n"
  },
  {
    "path": "src/pages/api/signer/[pubKey]/user.ts",
    "content": "import type { NextApiRequest, NextApiResponse } from 'next';\nimport { hexToBytes } from 'viem';\nimport { prisma } from '../../../../lib/prisma';\nimport { UserResponse } from '../../../../lib/types/user';\nimport { resolveUserFullFromFid } from '../../../../lib/user/resolve-user';\n\ntype SignerEndpointQuery = {\n  pubKey: `0x${string}`;\n};\n\nexport default async function signerUserEndpoint(\n  req: NextApiRequest,\n  res: NextApiResponse<UserResponse>\n): Promise<void> {\n  const { pubKey } = req.query as SignerEndpointQuery;\n\n  const signerRow = await prisma.signers.findFirst({\n    where: {\n      key: Buffer.from(hexToBytes(pubKey)),\n      removed_at: null\n    }\n  });\n\n  if (!signerRow) {\n    console.log(`Signer not found`);\n    res.status(404).json({\n      message: 'Signer not found'\n    });\n    return;\n  }\n\n  const user = await resolveUserFullFromFid(signerRow.fid);\n\n  if (!user) {\n    res.status(404).json({\n      message: 'User not found'\n    });\n    return;\n  }\n\n  res.json({ result: user });\n}\n"
  },
  {
    "path": "src/pages/api/topic/index.ts",
    "content": "import type { NextApiRequest, NextApiResponse } from 'next';\nimport { UserResponse } from '../../../lib/types/user';\nimport { resolveTopic } from '../../../lib/topics/resolve-topic';\nimport { TopicResponse } from '../../../lib/types/topic';\n\nexport default async function topicIdEndpoint(\n  req: NextApiRequest,\n  res: NextApiResponse<TopicResponse>\n): Promise<void> {\n  const topicUrl = decodeURIComponent(req.query.url as string);\n\n  const topic = await resolveTopic(topicUrl);\n\n  if (!topic) {\n    res.status(404).json({\n      message: 'Topic not found'\n    });\n    return;\n  }\n\n  res.json({ result: topic });\n}\n"
  },
  {
    "path": "src/pages/api/trends/index.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport { resolveTopic } from '../../../lib/topics/resolve-topic';\nimport { prisma } from '../../../lib/prisma';\nimport { TrendsResponse } from '../../../lib/types/trends';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<TrendsResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n\n      // Get casts in the last 4 hours and group by parent_url\n      const cutoffTime = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);\n      const results = await prisma.casts.groupBy({\n        by: ['parent_url'],\n        where: {\n          timestamp: {\n            gte: cutoffTime\n          },\n          parent_url: {\n            not: null\n          }\n        },\n        _count: {\n          hash: true\n        },\n        orderBy: {\n          _count: {\n            hash: 'desc'\n          }\n        },\n        take: limit\n      });\n\n      const topics = await Promise.all(\n        results.map(async (result) => {\n          const url = result.parent_url!;\n          const topic = await resolveTopic(url);\n          return { topic, volume: result._count.hash };\n        })\n      );\n\n      res.json({ result: topics });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/tweet/[id]/engagers.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport {\n  getReactionUsersPaginated,\n  PaginatedUsersResponse\n} from '../../../../lib/paginated-reactions';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<PaginatedUsersResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const id = req.query.id;\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : undefined;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      const type = parseInt(req.query.type as string);\n\n      const { users, nextPageCursor } = await getReactionUsersPaginated({\n        where: {\n          timestamp: {\n            lt: cursor || undefined\n          },\n          target_cast_hash: Buffer.from(id as string, 'hex'),\n          deleted_at: null,\n          type: type\n        },\n        take: limit,\n        orderBy: {\n          timestamp: 'desc' // reverse chronological order\n        },\n        distinct: 'fid'\n      });\n\n      res.json({\n        result: { users, nextPageCursor }\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/tweet/[id]/index.ts",
    "content": "import { ReactionType } from '@farcaster/hub-web';\nimport type { NextApiRequest, NextApiResponse } from 'next';\nimport { populateEmbed } from '../../../../lib/embeds';\nimport { prisma } from '../../../../lib/prisma';\nimport { resolveTopic } from '../../../../lib/topics/resolve-topic';\nimport { TopicType } from '../../../../lib/types/topic';\nimport {\n  ExternalEmbed,\n  Tweet,\n  tweetConverter,\n  TweetResponse,\n  TweetWithUsers\n} from '../../../../lib/types/tweet';\nimport {\n  resolveUserFromFid,\n  resolveUsersMap\n} from '../../../../lib/user/resolve-user';\nimport { JsonObject } from '@prisma/client/runtime/library';\nimport { convertAndCalculateCursor } from '@lib/paginated-tweets';\n\ntype TweetEndpointQuery = {\n  id: string;\n};\n\nexport default async function tweetIdEndpoint(\n  req: NextApiRequest,\n  res: NextApiResponse<TweetResponse>\n): Promise<void> {\n  const { id } = req.query as TweetEndpointQuery;\n\n  const cast = await prisma.casts.findUnique({\n    where: {\n      hash: Buffer.from(id, 'hex'),\n      deleted_at: null\n    }\n  });\n\n  if (!cast) {\n    res.status(404).json({\n      message: 'Cast not found'\n    });\n    return;\n  }\n\n  const { tweets, users } = await convertAndCalculateCursor([cast]);\n  const tweet = tweets[0];\n\n  const signer = await prisma.signers.findFirst({\n    where: {\n      key: cast.signer\n    }\n  });\n  const clientFid =\n    signer?.metadata_type === 1\n      ? ((signer?.metadata as JsonObject | undefined)?.requestFid as string) ||\n        null\n      : null;\n  const clientUser = clientFid\n    ? await resolveUserFromFid(BigInt(clientFid))\n    : null;\n\n  let topic: TopicType | null = null;\n  if (cast.parent_url) {\n    topic = await resolveTopic(cast.parent_url);\n  }\n\n  const tweetWithUsers: TweetWithUsers = {\n    ...tweet,\n    users,\n    topic: topic,\n    client: clientUser?.name || null\n  };\n\n  res.json({\n    result: tweetWithUsers\n  });\n}\n"
  },
  {
    "path": "src/pages/api/tweet/[id]/replies.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport {\n  getTweetsPaginatedPrismaArgs,\n  PaginatedTweetsResponse\n} from '../../../../lib/paginated-tweets';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<PaginatedTweetsResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const id = req.query.id;\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : undefined;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n\n      const result = await getTweetsPaginatedPrismaArgs({\n        where: {\n          timestamp: {\n            lt: cursor || undefined\n          },\n          parent_hash: Buffer.from(id as string, 'hex'),\n          deleted_at: null\n        },\n        take: limit,\n        orderBy: {\n          timestamp: 'desc' // reverse chronological order\n        }\n      });\n\n      res.json({\n        result\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/tweet/batch.ts",
    "content": "import { Prisma, casts } from '@prisma/client';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport {\n  PaginatedTweetsResponse,\n  PaginatedTweetsType,\n  TweetsResponse,\n  getTweetsPaginatedRawSql\n} from '../../../lib/paginated-tweets';\nimport { prisma } from '../../../lib/prisma';\nimport { tweetConverter } from '../../../lib/types/tweet';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<PaginatedTweetsResponse | TweetsResponse | string>\n) {\n  if (req.method === 'OPTIONS') {\n    return res.status(200).send('Ok');\n  }\n\n  const { method } = req;\n  switch (method) {\n    case 'POST':\n      const userFid = req.query.fid ? Number(req.query.fid) : null;\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : null;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      const skip = req.query.skip ? Number(req.query.skip) : 0;\n      const after = !!req.query.after && req.query.after !== 'false';\n      const full = !!req.query.full && req.query.full !== 'false';\n      const topicUrl = req.query.topic_url\n        ? decodeURIComponent(req.query.topic_url as string)\n        : null;\n\n      const castHashes = req.body.castHashes as string[];\n\n      // Get all the target_fids (people that the user follows)\n      let targetFids: bigint[] | null = null;\n      if (userFid != null) {\n        const links = await prisma.links.findMany({\n          where: {\n            fid: userFid,\n            target_fid: { not: undefined },\n            deleted_at: null\n          },\n          select: {\n            target_fid: true\n          }\n        });\n        targetFids = [\n          ...(links.map((link) => link.target_fid) as bigint[]),\n          BigInt(userFid)\n        ];\n      }\n\n      let reverseChronologicalQuery = Prisma.sql`SELECT * FROM casts\n      WHERE\n        hash IN (${Prisma.join(castHashes.map((h) => Buffer.from(h, 'hex')))})\n        AND parent_hash IS NULL\n        AND casts.deleted_at IS NULL\n      ORDER BY timestamp DESC`;\n\n      if (!full) {\n        const casts = await prisma.$queryRaw<casts[]>(\n          reverseChronologicalQuery\n        );\n        const tweets = casts.map(tweetConverter.toTweet);\n        res.json({\n          result: { tweets }\n        });\n        return;\n      }\n\n      let result: PaginatedTweetsType;\n\n      result = await getTweetsPaginatedRawSql(\n        reverseChronologicalQuery,\n        skip !== undefined\n          ? () => {\n              return (skip + limit).toString();\n            }\n          : undefined\n      );\n\n      // const tweetsWithEmbeds = await populateEmbedsForTweets(result.tweets);\n\n      res.json({\n        result: {\n          ...result,\n          tweets: result.tweets\n        }\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/index.ts",
    "content": "import type { NextApiRequest, NextApiResponse } from 'next';\nimport { UserFull, UserResponse } from '../../../../lib/types/user';\nimport { resolveUserAmbiguous } from '../../../../lib/user/resolve-user';\n\ntype UserEndpointQuery = {\n  id: string;\n  full?: string;\n};\n\nexport default async function userIdEndpoint(\n  req: NextApiRequest,\n  res: NextApiResponse<UserResponse>\n): Promise<void> {\n  const { id, full = 'true' } = req.query as UserEndpointQuery;\n\n  const user = (await resolveUserAmbiguous(id, full === 'true')) as UserFull;\n\n  if (!user) {\n    res.status(404).json({\n      message: 'User not found'\n    });\n    return;\n  }\n\n  res.json({ result: user });\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/interests.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport { prisma } from '../../../../lib/prisma';\nimport { resolveTopic } from '../../../../lib/topics/resolve-topic';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const fid = parseInt(req.query.id as string);\n\n      if (!fid) {\n        res.status(400).json({ error: 'Missing id' });\n        return;\n      }\n\n      // const castGroups = await prisma.casts.groupBy({\n      //   by: ['parent_url'],\n      //   _count: {\n      //     hash: true\n      //   },\n      //   where: {\n      //     fid: fid,\n      //     deleted_at: null,\n      //     parent_url: {\n      //       not: null\n      //     }\n      //   },\n      //   take: 5,\n      //   orderBy: {\n      //     _count: {\n      //       hash: 'desc'\n      //     }\n      //   }\n      // });\n\n      const reactionGroups = (await prisma.$queryRaw`\n        SELECT \n            c.parent_url, \n            COUNT(*) as reaction_count \n        FROM \n            reactions r\n        INNER JOIN \n            casts c ON r.target_cast_hash = c.hash \n        WHERE \n            r.fid = ${fid}  \n            AND c.deleted_at IS NULL \n            AND c.parent_url IS NOT NULL \n        GROUP BY c.parent_url\n        ORDER BY reaction_count DESC\n        LIMIT 5;\n      `) as { parent_url: string; reaction_count: number }[];\n\n      const topics = await Promise.all(\n        reactionGroups.map(async (group) => {\n          const url = group.parent_url!;\n          const topic = await resolveTopic(url);\n          return { topic, volume: Number(group.reaction_count) };\n        })\n      );\n\n      res.json({ topics });\n\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/known-followers.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport { prisma } from '../../../../lib/prisma';\nimport { KnownFollowersResponse } from '../../../../lib/types/user';\nimport { resolveUsers } from '../../../../lib/user/resolve-user';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<KnownFollowersResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const fid = parseInt(req.query.id as string);\n      const contextFid = parseInt(req.query.context_id as string);\n\n      // Get all the target_fids (people that the user follows)\n      const contextFollowing = await prisma.links.findMany({\n        where: {\n          fid: contextFid,\n          target_fid: { not: undefined },\n          deleted_at: null\n        },\n        select: {\n          target_fid: true\n        }\n      });\n\n      const links = await prisma.links.findMany({\n        where: {\n          deleted_at: null,\n          type: 'follow',\n          fid: {\n            in: contextFollowing.map((link) => link.target_fid!)\n          },\n          target_fid: fid\n        },\n        distinct: ['target_fid', 'fid'],\n        select: {\n          fid: true\n        },\n        orderBy: {\n          timestamp: 'desc'\n        }\n      });\n\n      // TODO: Resolve the most popular users\n      const resolvedUsers = await resolveUsers(\n        links.slice(0, 6).map((link) => link.fid!)\n      );\n\n      res.json({\n        result: { knownFollowerCount: links.length, resolvedUsers }\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/likes.ts",
    "content": "import { ReactionType, UserDataType } from '@farcaster/hub-web';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport {\n  getTweetsPaginatedPrismaArgs,\n  PaginatedTweetsResponse\n} from '../../../../lib/paginated-tweets';\nimport { prisma } from '../../../../lib/prisma';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<PaginatedTweetsResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : undefined;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n\n      // Try to convert id to number\n      let id = req.query.id;\n      if (isNaN(Number(id))) {\n        const username = (id as string).toLowerCase();\n        const userData = await prisma.user_data.findFirst({\n          where: {\n            type: UserDataType.USERNAME,\n            value: username\n          }\n        });\n        if (userData) {\n          id = userData.fid.toString();\n        } else {\n          res.status(404).json({ message: 'User not found' });\n          return;\n        }\n      }\n\n      const reactions = await prisma.reactions.findMany({\n        where: {\n          timestamp: {\n            lt: cursor || undefined\n          },\n          fid: BigInt(id as string),\n          deleted_at: null,\n          type: ReactionType.LIKE\n        },\n        take: limit,\n        orderBy: {\n          timestamp: 'desc' // reverse chronological order\n        }\n      });\n\n      const result = await getTweetsPaginatedPrismaArgs({\n        where: {\n          hash: {\n            in: reactions\n              .map((reaction) => reaction.target_cast_hash)\n              .filter((hash) => hash !== null) as Buffer[]\n          },\n          deleted_at: null\n        },\n        take: limit,\n        orderBy: {\n          timestamp: 'desc' // reverse chronological order\n        }\n      });\n\n      res.json({\n        result\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/links.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport { PaginatedUsersResponse } from '../../../../lib/paginated-reactions';\nimport { prisma } from '../../../../lib/prisma';\nimport { resolveUsers } from '../../../../lib/user/resolve-user';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<PaginatedUsersResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const fid = parseInt(req.query.id as string);\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : undefined;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      const type = req.query.type as 'following' | 'followers';\n\n      const links = await prisma.links.findMany({\n        where: {\n          timestamp: {\n            lt: cursor || undefined\n          },\n          deleted_at: null,\n          type: 'follow',\n          fid: type === 'following' ? fid : undefined,\n          target_fid: type === 'followers' ? fid : undefined\n        },\n        take: limit,\n        distinct: ['target_fid', 'fid'],\n        orderBy: {\n          timestamp: 'desc'\n        }\n      });\n\n      const users = await resolveUsers(\n        type === 'following'\n          ? links.map((link) => link.target_fid!)\n          : links.map((link) => link.fid!)\n      );\n\n      const nextPageCursor =\n        links.length > 0\n          ? links[links.length - 1].timestamp.toISOString()\n          : null;\n\n      res.json({\n        result: { users, nextPageCursor }\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/notifications.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport { castsToTweets } from '../../../../lib/paginated-tweets';\nimport { prisma } from '../../../../lib/prisma';\nimport {\n  AccumulatedFollow,\n  AccumulatedReaction,\n  BasicFollow,\n  BasicMention,\n  BasicNotification,\n  BasicReaction,\n  BasicReply,\n  FollowerQueryResult,\n  MentionsQueryResult,\n  NotificationsResponseFull,\n  NotificationsResponseSummary,\n  ReactionQueryResult,\n  RepliesQueryResult\n} from '../../../../lib/types/notifications';\nimport { Tweet, tweetConverter } from '../../../../lib/types/tweet';\nimport { User, UsersMapType } from '../../../../lib/types/user';\nimport { resolveUsersMap } from '../../../../lib/user/resolve-user';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<NotificationsResponseFull | NotificationsResponseSummary>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const fid = parseInt(req.query.id as string);\n      const cursor =\n        req.query.cursor && req.query.cursor !== 'undefined'\n          ? Number(req.query.cursor)\n          : 0;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      let afterTime = req.query.last_time\n        ? new Date(req.query.last_time as string)\n        : null;\n      const beforeTime = req.query.before_time\n        ? new Date(req.query.before_time as string)\n        : null;\n      const full = !!req.query.full && req.query.full !== 'false';\n\n      if (!fid) {\n        res.status(400).json({ message: 'Missing id' });\n        return;\n      }\n\n      if (!(afterTime || beforeTime)) {\n        res.status(400).json({ message: 'Missing time' });\n        return;\n      }\n\n      if (beforeTime) {\n        // Last time = before time - 24 hours\n        afterTime = new Date(beforeTime.getTime() - 24 * 60 * 60 * 1000);\n      }\n\n      // Get last reaction and last cast from other clients\n      const lastActivity = (await prisma.$queryRaw`\n        SELECT timestamp FROM (\n          SELECT 'cast' as type, id, fid, timestamp\n          FROM casts\n          WHERE \n            fid = ${fid} AND\n            deleted_at IS NULL AND\n            timestamp > ${afterTime}\n          UNION ALL\n          SELECT 'reaction' as type, id, fid, timestamp\n          FROM reactions\n          WHERE \n            fid = ${fid} AND\n            deleted_at IS NULL AND\n            timestamp > ${afterTime}\n        ) AS combined\n        LEFT JOIN signers ON combined.fid = signers.fid\n        WHERE \n          (signers.requester_fid IS NULL OR signers.requester_fid != ${BigInt(\n            process.env.APP_FID!\n          )})\n        ORDER BY timestamp DESC\n        LIMIT 1\n      `) as { timestamp: Date }[];\n\n      // Consider that user would likely have cleared notifications badge on the other client\n      // afterTime is the max of last activity or afterTime\n      if (lastActivity.length > 0 && req.query.last_time) {\n        afterTime = new Date(\n          Math.max(\n            afterTime?.getTime() || 0,\n            lastActivity[0].timestamp.getTime()\n          )\n        );\n      }\n\n      const userPostsReactions = (await prisma.$queryRaw`\n        SELECT \n          casts.*, \n          reactions.type as reaction_type, \n          reactions.fid as message_fid, \n          reactions.hash as message_hash, \n          reactions.timestamp as message_timestamp,\n          3 as message_type\n        FROM casts \n        JOIN reactions ON casts.hash = reactions.target_cast_hash \n        WHERE\n            casts.fid = ${fid} AND\n            reactions.deleted_at IS NULL AND\n            reactions.timestamp > ${afterTime}\n        ORDER BY reactions.timestamp DESC;\n      `) as ReactionQueryResult[];\n\n      const userNewFollowers = (await prisma.$queryRaw`\n        SELECT \n          links.fid as message_fid, \n          links.hash as message_hash, \n          5 as message_type,\n          links.timestamp as message_timestamp\n        FROM links\n        WHERE\n            links.target_fid = ${fid} AND\n            links.type = 'follow' AND\n            links.deleted_at IS NULL AND\n            links.timestamp > ${afterTime};\n      `) as FollowerQueryResult[];\n\n      const userPostsReplies = (await prisma.$queryRaw`\n        SELECT replies.*, \n        replies.fid as message_fid, \n        replies.hash as message_hash, \n        replies.timestamp as message_timestamp,\n        1 as message_type,\n        casts.fid as parent_fid \n        FROM casts as replies\n        JOIN casts ON casts.hash = replies.parent_hash\n        WHERE \n            casts.fid = ${fid} AND\n            replies.deleted_at IS NULL AND\n            casts.deleted_at IS NULL AND\n            replies.timestamp > ${afterTime};\n      `) as RepliesQueryResult[];\n\n      const userMentions = (await prisma.$queryRaw`\n        SELECT casts.*, \n        casts.fid as message_fid, \n        casts.hash as message_hash, \n        casts.timestamp as message_timestamp,\n        1 as message_type\n        FROM casts\n        CROSS JOIN LATERAL json_array_elements_text(casts.mentions) as mention\n        WHERE\n            casts.deleted_at IS NULL AND\n            casts.timestamp > ${afterTime} AND\n            mention::INTEGER = ${fid};`) as MentionsQueryResult[];\n\n      const badgeCount =\n        userNewFollowers.length +\n        userPostsReactions.length +\n        userPostsReplies.length +\n        userMentions.length;\n\n      if (!full) {\n        res.json({\n          result: {\n            badgeCount,\n            lastChecked: new Date().toISOString()\n          }\n        });\n        return;\n      }\n\n      const fids: Set<bigint> = new Set<bigint>();\n      [\n        ...userPostsReactions,\n        ...userNewFollowers,\n        ...userPostsReplies,\n        ...userMentions\n      ].forEach((item) => {\n        fids.add(item.message_fid);\n        if ('parent_fid' in item) {\n          if (item.parent_fid)\n            fids.add((item as { parent_fid: bigint }).parent_fid);\n        }\n        if ('mentions' in item) {\n          ((item as any as RepliesQueryResult).mentions as number[]).forEach(\n            (mention) => fids.add(BigInt(mention))\n          );\n        }\n      });\n      const usersMap: UsersMapType<User> = await resolveUsersMap([...fids]);\n\n      const { tweets: tweetsWithStats } = await castsToTweets([\n        ...userPostsReplies,\n        ...userMentions\n      ]);\n      const tweetsNoStats = userPostsReactions.map((cast) =>\n        tweetConverter.toTweet(cast)\n      );\n      const tweetsMap = [...tweetsNoStats, ...tweetsWithStats].reduce(\n        (acc: { [key: string]: Tweet }, cur) => {\n          acc[cur.id] = cur;\n          return acc;\n        },\n        {}\n      );\n\n      const basicReactions: BasicReaction[] = userPostsReactions.map(\n        (reaction) => ({\n          userId: reaction.message_fid.toString(),\n          timestamp: reaction.message_timestamp,\n          targetCastId: reaction.hash.toString('hex'),\n          messageType: reaction.message_type,\n          reactionType: reaction.reaction_type\n        })\n      );\n\n      const basicFollows: BasicFollow[] = userNewFollowers.map((follower) => ({\n        userId: follower.message_fid.toString(),\n        timestamp: follower.message_timestamp,\n        messageType: follower.message_type\n      }));\n\n      const basicReplies: BasicReply[] = userPostsReplies.map((reply) => ({\n        userId: reply.message_fid.toString(),\n        timestamp: reply.message_timestamp,\n        castId: reply.hash.toString('hex'),\n        messageType: reply.message_type,\n        parentUserId: reply.parent_fid.toString()\n      }));\n\n      const basicMentions: BasicMention[] = userMentions.map((mention) => ({\n        userId: mention.message_fid.toString(),\n        timestamp: mention.message_timestamp,\n        castId: mention.hash.toString('hex'),\n        messageType: mention.message_type\n      }));\n\n      /* Combine into notifications */\n\n      // Group reactions by cast\n      const reactionsByCast = basicReactions.reduce(\n        (\n          acc: {\n            [key: string]: AccumulatedReaction;\n          },\n          item\n        ) => {\n          const key = `${item.targetCastId}-${item.reactionType}`;\n          if (!acc[key])\n            acc[key] = {\n              castId: item.targetCastId,\n              reactions: [],\n              ...item\n            };\n          acc[key].reactions.push(item);\n          if (item.timestamp > acc[key].timestamp) {\n            acc[key].timestamp = item.timestamp;\n            acc[key].userId = item.userId;\n          }\n          return acc;\n        },\n        {}\n      );\n\n      const sortedFollows = basicFollows.sort((a, b) => {\n        return (\n          new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()\n        );\n      });\n      const accumulatedFollow: AccumulatedFollow = {\n        ...(sortedFollows[0] || {}),\n        follows: sortedFollows\n      };\n\n      // Combine all notifications\n      const allNotifications: BasicNotification[] = [\n        ...Object.values(reactionsByCast),\n        ...(sortedFollows.length > 0 ? [accumulatedFollow] : []),\n        ...basicReplies,\n        ...basicMentions\n      ];\n\n      // Sort by time\n      allNotifications.sort((a, b) => {\n        return (\n          new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()\n        );\n      });\n\n      res.json({\n        result: {\n          notifications: allNotifications.slice(cursor, cursor + limit),\n          tweetsMap,\n          usersMap,\n          badgeCount,\n          lastChecked: new Date().toISOString(),\n          cursor:\n            cursor + limit < allNotifications.length ? cursor + limit : null\n        }\n      });\n\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/signers/[pubKey]/backup.ts",
    "content": "import type { NextApiRequest, NextApiResponse } from 'next';\nimport { prisma } from '../../../../../../lib/prisma';\nimport { serialize } from '../../../../../../lib/utils';\nimport { MessagesArchiveResponse } from '../../../../../../lib/types/signer';\nimport { Message } from '@farcaster/hub-nodejs';\nimport { getSignerDetail } from '../../../../../../lib/signers';\n\ntype SignerEndpointQuery = {\n  pubKey: string;\n  id: string;\n};\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse<MessagesArchiveResponse>\n): Promise<void> {\n  const { pubKey } = req.query as SignerEndpointQuery;\n\n  const signer = await getSignerDetail(pubKey);\n\n  if (!signer)\n    return res.status(404).json({\n      message: 'Signer not found'\n    });\n\n  const pubKeyBytes = Buffer.from(pubKey, 'hex');\n  // const messageRows = await prisma.messages.findMany({\n  //   where: {\n  //     signer: pubKeyBytes\n  //   },\n  //   distinct: ['hash']\n  // });\n\n  // const messages = messageRows.map((m) =>\n  //   Message.toJSON(Message.decode(m.raw))\n  // );\n\n  return res.json({\n    result: {\n      messages: [],\n      signer: serialize(signer)\n    }\n  });\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/signers/[pubKey]/casts.ts",
    "content": "import { Prisma } from '@prisma/client';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport {\n  PaginatedTweetsType,\n  getTweetsPaginatedRawSql\n} from '../../../../../../lib/paginated-tweets';\nimport { BaseResponse } from '../../../../../../lib/types/responses';\n\ntype Query = {\n  pubKey: string;\n  id: string;\n};\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<BaseResponse<PaginatedTweetsType>>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const { pubKey, id } = req.query as Query;\n      const fid = parseInt(req.query.id as string);\n      const pubKeyBytes = Buffer.from(pubKey, 'hex');\n\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      const skip = req.query.skip ? Number(req.query.skip) : 0;\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : null;\n      const after = !!req.query.after && req.query.after !== 'false';\n      const full = !!req.query.full && req.query.full !== 'false';\n\n      if (after || !full) {\n        res.json({\n          result: {\n            nextPageCursor: null,\n            tweets: [],\n            users: {}\n          }\n        });\n        return;\n      }\n\n      let result: PaginatedTweetsType;\n\n      result = await getTweetsPaginatedRawSql(\n        Prisma.sql`\n        SELECT * FROM messages m\n        INNER JOIN casts c ON m.hash = c.hash\n        WHERE\n          signer = ${pubKeyBytes} AND\n          (${cursor}::timestamp IS NULL OR c.timestamp < ${cursor}::timestamp)\n        ORDER BY c.timestamp DESC\n        LIMIT ${limit}\n        OFFSET ${skip}\n      `,\n        skip !== undefined\n          ? () => {\n              return (skip + limit).toString();\n            }\n          : undefined\n      );\n\n      res.json({\n        result: {\n          ...result\n        }\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/signers/[pubKey]/index.ts",
    "content": "import type { NextApiRequest, NextApiResponse } from 'next';\nimport { getSignerDetail } from '../../../../../../lib/signers';\nimport { serialize } from '../../../../../../lib/utils';\n\ntype SignerEndpointQuery = {\n  pubKey: string;\n  id: string;\n};\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse\n): Promise<void> {\n  const { pubKey, id: userId } = req.query as SignerEndpointQuery;\n\n  const signer = await getSignerDetail(pubKey);\n\n  if (!signer) {\n    console.log(`Signer not found`);\n    res.status(404).json({\n      message: 'Signer not found'\n    });\n    return;\n  }\n\n  return res.json({ result: serialize(signer) });\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/signers/index.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport { prisma } from '../../../../../lib/prisma';\nimport { SignersResponse } from '../../../../../lib/types/signer';\nimport { serialize } from '../../../../../lib/utils';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<SignersResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const fid = parseInt(req.query.id as string);\n\n      if (!fid) {\n        res.status(400).json({ message: 'Missing id' });\n        return;\n      }\n\n      const signersRaw = await prisma.$queryRaw<any>`\n        SELECT\n          COUNT(DISTINCT m.hash) AS message_count,\n          MAX(m.timestamp) AS last_message_timestamp,\n          MAX(s.created_at) AS signer_created_at,\n          MAX(s.timestamp) AS signer_timestamp,\n          MAX(s.name) as signer_name,\n          s.signer as pubkey,\n          COUNT(DISTINCT CASE WHEN m.type = 0 THEN m.hash ELSE NULL END) AS none_count,\n          COUNT(DISTINCT CASE WHEN m.type = 1 THEN m.hash ELSE NULL END) AS cast_add_count,\n          COUNT(DISTINCT CASE WHEN m.type = 2 THEN m.hash ELSE NULL END) AS cast_remove_count,\n          COUNT(DISTINCT CASE WHEN m.type = 3 THEN m.hash ELSE NULL END) AS reaction_add_count,\n          COUNT(DISTINCT CASE WHEN m.type = 4 THEN m.hash ELSE NULL END) AS reaction_remove_count,\n          COUNT(DISTINCT CASE WHEN m.type = 5 THEN m.hash ELSE NULL END) AS link_add_count,\n          COUNT(DISTINCT CASE WHEN m.type = 6 THEN m.hash ELSE NULL END) AS link_remove_count,\n          COUNT(DISTINCT CASE WHEN m.type = 7 THEN m.hash ELSE NULL END) AS verification_add_eth_address_count,\n          COUNT(DISTINCT CASE WHEN m.type = 8 THEN m.hash ELSE NULL END) AS verification_remove_count\n        FROM \n          messages m\n        LEFT JOIN \n          signers s ON m.signer = s.signer\n        WHERE \n          m.fid = ${fid}\n        GROUP BY \n          m.signer, s.signer\n        ORDER BY last_message_timestamp DESC;\n      `;\n\n      const signers = signersRaw\n        .filter((s: any) => s.pubkey)\n        .map((signer: any) => ({\n          pubKey: signer.pubkey,\n          messageCount: signer.message_count,\n          // TODO: Fix this\n          createdAtTimestamp:\n            signer.signer_timestamp || signer.signer_created_at,\n          lastMessageTimestamp: signer.last_message_timestamp,\n          name: signer.signer_name,\n          noneCount: signer.none_count,\n          castAddCount: signer.cast_add_count,\n          castRemoveCount: signer.cast_remove_count,\n          reactionAddCount: signer.reaction_add_count,\n          reactionRemoveCount: signer.reaction_remove_count,\n          linkAddCount: signer.link_add_count,\n          linkRemoveCount: signer.link_remove_count,\n          verificationAddEthAddressCount:\n            signer.verification_add_eth_address_count,\n          verificationRemoveCount: signer.verification_remove_count\n        }));\n\n      return res.json({ result: serialize(signers) });\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/sync.ts",
    "content": "import type { NextApiRequest, NextApiResponse } from 'next';\nimport { UserFull, UserResponse } from '../../../../lib/types/user';\n\ntype UserEndpointQuery = {\n  id: string;\n};\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse<UserResponse>\n): Promise<void> {\n  const { id } = req.query as UserEndpointQuery;\n\n  const syncProgressRes = await fetch(\n    `${process.env.INDEXER_API_URL!}/root-backfill/${id}`\n  );\n\n  if (!syncProgressRes.ok) {\n    const json = await syncProgressRes.json();\n    res.status(syncProgressRes.status).json(json);\n    return;\n  }\n\n  const syncProgress = await syncProgressRes.json();\n\n  res.status(200).json(syncProgress);\n}\n"
  },
  {
    "path": "src/pages/api/user/[id]/tweets.ts",
    "content": "import { UserDataType } from '@farcaster/hub-web';\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport {\n  getTweetsPaginatedPrismaArgs,\n  PaginatedTweetsResponse\n} from '../../../../lib/paginated-tweets';\nimport { prisma } from '../../../../lib/prisma';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<PaginatedTweetsResponse>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      const cursor = req.query.cursor\n        ? new Date(req.query.cursor as string)\n        : undefined;\n      const limit =\n        req.query.limit && req.query.limit !== 'undefined'\n          ? Number(req.query.limit)\n          : 10;\n      const replies = req.query.replies === 'true';\n\n      // Try to convert id to number\n      let id = req.query.id;\n      if (isNaN(Number(id))) {\n        const username = (id as string).toLowerCase();\n        const userData = await prisma.user_data.findFirst({\n          where: {\n            type: UserDataType.USERNAME,\n            value: username\n          }\n        });\n        if (userData) {\n          id = userData.fid.toString();\n        } else {\n          res.status(404).json({ message: 'User not found' });\n          return;\n        }\n      }\n\n      const result = await getTweetsPaginatedPrismaArgs({\n        where: {\n          timestamp: {\n            lt: cursor || undefined\n          },\n          fid: BigInt(id as string),\n          parent_hash: replies ? undefined : null,\n          deleted_at: null\n        },\n        take: limit,\n        orderBy: {\n          timestamp: 'desc' // reverse chronological order\n        }\n      });\n\n      res.json({\n        result\n      });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/api/user/resolve-usernames.ts",
    "content": "import { NextApiRequest, NextApiResponse } from 'next';\nimport { BaseResponse } from '../../../lib/types/responses';\nimport { prisma } from '../../../lib/prisma';\nimport { UserDataType } from '@farcaster/hub-nodejs';\n\nexport default async function handle(\n  req: NextApiRequest,\n  res: NextApiResponse<BaseResponse<{ [key: string]: number }>>\n) {\n  const { method } = req;\n  switch (method) {\n    case 'GET':\n      let usernames = (req.query.usernames as string).split(',');\n\n      const userData = await prisma.user_data.findMany({\n        where: {\n          type: UserDataType.USERNAME,\n          value: {\n            in: usernames\n          }\n        }\n      });\n\n      const usernameToId: { [key: string]: number } = userData.reduce(\n        (acc: { [key: string]: number }, cur) => {\n          acc[cur.value] = Number(cur.fid);\n          return acc;\n        },\n        {}\n      );\n\n      res.json({ result: usernameToId });\n      break;\n    default:\n      res.setHeader('Allow', ['GET']);\n      res.status(405).end(`Method ${method} Not Allowed`);\n  }\n}\n"
  },
  {
    "path": "src/pages/bookmarks.tsx.bak",
    "content": "import { useMemo } from 'react';\nimport { AnimatePresence } from 'framer-motion';\nimport { toast } from 'react-hot-toast';\nimport { orderBy, query } from 'firebase/firestore';\nimport { useAuth } from '@lib/context/auth-context';\nimport { useModal } from '@lib/hooks/useModal';\nimport { useCollection } from '@lib/hooks/useCollection';\nimport { useArrayDocument } from '@lib/hooks/useArrayDocument';\nimport { clearAllBookmarks } from '@lib/firebase/utils';\nimport {\n  tweetsCollection,\n  userBookmarksCollection\n} from '@lib/firebase/collections';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { SEO } from '@components/common/seo';\nimport { MainHeader } from '@components/home/main-header';\nimport { MainContainer } from '@components/home/main-container';\nimport { Modal } from '@components/modal/modal';\nimport { ActionModal } from '@components/modal/action-modal';\nimport { Tweet } from '@components/tweet/tweet';\nimport { StatsEmpty } from '@components/tweet/stats-empty';\nimport { Button } from '@components/ui/button';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport { Loading } from '@components/ui/loading';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function Bookmarks(): JSX.Element {\n  const { user } = useAuth();\n\n  const { open, openModal, closeModal } = useModal();\n\n  const userId = user?.id as string;\n\n  const { data: bookmarksRef, loading: bookmarksRefLoading } = useCollection(\n    query(userBookmarksCollection(userId), orderBy('createdAt', 'desc')),\n    { allowNull: true }\n  );\n\n  const tweetIds = useMemo(\n    () => bookmarksRef?.map(({ id }) => id) ?? [],\n    [bookmarksRef]\n  );\n\n  const { data: tweetData, loading: tweetLoading } = useArrayDocument(\n    tweetIds,\n    tweetsCollection,\n    { includeUser: true }\n  );\n\n  const handleClear = async (): Promise<void> => {\n    await clearAllBookmarks(userId);\n    closeModal();\n    toast.success('Successfully cleared all bookmarks');\n  };\n\n  return (\n    <MainContainer>\n      <SEO title='Bookmarks / Twitter' />\n      <Modal\n        modalClassName='max-w-xs bg-main-background w-full p-8 rounded-2xl'\n        open={open}\n        closeModal={closeModal}\n      >\n        <ActionModal\n          title='Clear all Bookmarks?'\n          description='This can’t be undone and you’ll remove all Tweets you’ve added to your Bookmarks.'\n          mainBtnClassName='bg-accent-red hover:bg-accent-red/90 active:bg-accent-red/75 accent-tab \n                            focus-visible:bg-accent-red/90'\n          mainBtnLabel='Clear'\n          action={handleClear}\n          closeModal={closeModal}\n        />\n      </Modal>\n      <MainHeader className='flex items-center justify-between'>\n        <div className='-mb-1 flex flex-col'>\n          <h2 className='-mt-1 text-xl font-bold'>Bookmarks</h2>\n          <p className='text-xs text-light-secondary dark:text-dark-secondary'>\n            @{user?.username}\n          </p>\n        </div>\n        <Button\n          className='dark-bg-tab group relative p-2 hover:bg-light-primary/10\n                     active:bg-light-primary/20 dark:hover:bg-dark-primary/10 \n                     dark:active:bg-dark-primary/20'\n          onClick={openModal}\n        >\n          <HeroIcon className='h-5 w-5' iconName='ArchiveBoxXMarkIcon' />\n          <ToolTip\n            className='!-translate-x-20 translate-y-3 md:-translate-x-1/2'\n            tip='Clear bookmarks'\n          />\n        </Button>\n      </MainHeader>\n      <section className='mt-0.5'>\n        {bookmarksRefLoading || tweetLoading ? (\n          <Loading className='mt-5' />\n        ) : !bookmarksRef ? (\n          <StatsEmpty\n            title='Save Tweets for later'\n            description='Don’t let the good ones fly away! Bookmark Tweets to easily find them again in the future.'\n            imageData={{ src: '/assets/no-bookmarks.png', alt: 'No bookmarks' }}\n          />\n        ) : (\n          <AnimatePresence mode='popLayout'>\n            {tweetData?.map((tweet) => (\n              <Tweet {...tweet} key={tweet.id} />\n            ))}\n          </AnimatePresence>\n        )}\n      </section>\n    </MainContainer>\n  );\n}\n\nBookmarks.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/home.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { Input } from '@components/input/input';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { useWindow } from '@lib/context/window-context';\nimport { useState, type ReactElement, type ReactNode } from 'react';\nimport useSWR from 'swr';\nimport { TweetFeed } from '../components/feed/tweet-feed';\nimport { FeedOrderingSelector } from '../components/ui/feed-ordering-selector';\nimport { UserAvatar } from '../components/user/user-avatar';\nimport { useAuth } from '../lib/context/auth-context';\nimport { fetchJSON } from '../lib/fetch';\nimport { FeedOrderingType } from '../lib/types/feed';\nimport { OnlineUsersResponse } from '../lib/types/online';\nimport { NextImage } from '../components/ui/next-image';\n\nexport default function Home(): JSX.Element {\n  const { isMobile } = useWindow();\n  const { user, userNotifications } = useAuth();\n\n  const { data: onlineResponse, isValidating: onlineUsersLoading } = useSWR(\n    `/api/online?fid=${user?.id}`,\n    async (url) => (await fetchJSON<OnlineUsersResponse>(url)).result,\n    { revalidateOnFocus: false, refreshInterval: 10_000 }\n  );\n\n  const [feedOrdering, setFeedOrdering] = useState<FeedOrderingType>('latest');\n\n  return (\n    <MainContainer>\n      <SEO\n        title={`${\n          userNotifications && user?.keyPair ? `(${userNotifications}) ` : ''\n        }Home / Opencast`}\n      />\n      <MainHeader\n        useMobileSidebar\n        title='Home'\n        className='flex items-center justify-between'\n      ></MainHeader>\n      <div className='overflow-scroll'>\n        <div>\n          {onlineUsersLoading && !onlineResponse && (\n            <div className='p-1'>\n              <NextImage\n                useSkeleton\n                className='overflow-hidden rounded-full'\n                imgClassName='rounded-full !h-full !w-full'\n                width={64}\n                height={64}\n                src={''}\n                alt={''}\n                key={'loading'}\n              />\n            </div>\n          )}\n\n          <div className='flex gap-2 px-2'>\n            {onlineResponse && onlineResponse.users?.length === 0 && (\n              <div>No users online</div>\n            )}\n            {onlineResponse &&\n              onlineResponse.users?.map(({ user, appFid }) => (\n                <div key={user.id} className='p-1'>\n                  <div className='relative rounded-full bg-gradient-to-r from-blue-500 to-purple-500 p-[2px]'>\n                    <div className='relative rounded-full bg-white'>\n                      <UserAvatar\n                        username={user.username}\n                        src={user.photoURL}\n                        alt={user.name}\n                        size={64}\n                      />\n                      <div className='absolute bottom-0.5 right-0.5 h-2 w-2 rounded-full bg-green-500'></div>\n                      {onlineResponse.appProfilesMap[appFid] && (\n                        <img\n                          className='border-1 absolute bottom-0.5 h-5 w-5 rounded-md border border-gray-500'\n                          src={onlineResponse.appProfilesMap[appFid].pfp}\n                          alt={onlineResponse.appProfilesMap[appFid].display}\n                        />\n                      )}\n                    </div>\n                  </div>\n                </div>\n              ))}\n          </div>\n        </div>\n      </div>\n      <FeedOrderingSelector {...{ feedOrdering, setFeedOrdering }} />\n      {!isMobile && user?.keyPair && <Input />}\n      <TweetFeed\n        feedOrdering={feedOrdering}\n        apiEndpoint={`/api/feed?fid=${user?.id}`}\n      />\n    </MainContainer>\n  );\n}\n\nHome.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/index.tsx",
    "content": "import { AuthLayout } from '@components/layout/auth-layout';\nimport type { ReactElement, ReactNode } from 'react';\nimport Login from './login';\n\nexport default function Landing(): JSX.Element {\n  return <Login />;\n}\n\nLanding.getLayout = (page: ReactElement): ReactNode => (\n  <AuthLayout>{page}</AuthLayout>\n);\n"
  },
  {
    "path": "src/pages/login.tsx",
    "content": "import { AuthLayout } from '@components/layout/auth-layout';\nimport { SEO } from '@components/common/seo';\nimport { LoginMain } from '@components/login/login-main';\nimport { LoginFooter } from '@components/login/login-footer';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function Login(): JSX.Element {\n  return (\n    <div className='grid min-h-screen grid-rows-[1fr,auto]'>\n      <SEO\n        title='Opencast - Fully open source Twitter flavoured Farcaster client'\n        description='From breaking news and entertainment to sports and politics, get the full story with all the live commentary.'\n      />\n      <LoginMain />\n      <LoginFooter />\n    </div>\n  );\n}\n\nLogin.getLayout = (page: ReactElement): ReactNode => (\n  <AuthLayout forceLogin={true}>{page}</AuthLayout>\n);\n"
  },
  {
    "path": "src/pages/notifications.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { Error } from '@components/ui/error';\nimport { Loading } from '@components/ui/loading';\nimport { MessageType, ReactionType } from '@farcaster/hub-web';\nimport Link from 'next/link';\nimport { useEffect, useState, type ReactElement, type ReactNode } from 'react';\nimport useSWRInfinite from 'swr/infinite';\nimport { LoadMoreSentinel } from '../components/common/load-more';\nimport { Tweet as TweetView } from '../components/tweet/tweet';\nimport { splitAndInsert } from '../components/tweet/tweet-text';\nimport { HeroIcon } from '../components/ui/hero-icon';\nimport { UserAvatar } from '../components/user/user-avatar';\nimport { UserName } from '../components/user/user-name';\nimport { UserTooltip } from '../components/user/user-tooltip';\nimport { useAuth } from '../lib/context/auth-context';\nimport {\n  AccumulatedFollow,\n  AccumulatedReaction,\n  BasicMention,\n  BasicNotification,\n  BasicReply,\n  NotificationsResponseFull\n} from '../lib/types/notifications';\nimport { populateTweetUsers } from '../lib/types/tweet';\nimport { isPlural } from '../lib/utils';\n\nexport default function NotificationsPage(): JSX.Element {\n  const { user, resetNotifications } = useAuth();\n\n  const [lastCheckedNotifications, setLastCheckedNotifications] = useState(\n    new Date()\n  );\n\n  const {\n    data: pages,\n    size,\n    setSize,\n    isValidating: loading,\n    error\n  } = useSWRInfinite<NotificationsResponseFull>(\n    (pageIndex, prevPage) => {\n      if (!user) return null;\n\n      if (prevPage && !prevPage.result?.cursor) return null;\n\n      const baseUrl = `/api/user/${user.id\n        }/notifications?before_time=${lastCheckedNotifications.toISOString()}&full=true`;\n\n      if (pageIndex === 0) return baseUrl;\n\n      if (!prevPage?.result) return null;\n\n      return `${baseUrl}&cursor=${prevPage.result.cursor}&limit=10`;\n    },\n    { revalidateFirstPage: false }\n  );\n\n  const hasMore = !!pages?.[size - 1]?.result?.notifications.length;\n\n  useEffect(() => {\n    resetNotifications();\n    console.log(lastCheckedNotifications.toISOString());\n  }, []);\n\n  return (\n    <MainContainer>\n      <SEO title={`Notifications / Opencast`} />\n      <MainHeader\n        useMobileSidebar\n        title='Recent Notifications'\n        className='flex items-center justify-between'\n      ></MainHeader>\n      {!!!user?.keyPair ? (\n        <Error message='User not signed in' />\n      ) : (\n        <>\n          <section className='mt-0.5 xs:mt-0'>\n            {error ? (\n              <Error message='Something went wrong' />\n            ) : (\n              pages && (\n                <div>\n                  {pages.map(\n                    ({ result: data }, pageIndex) =>\n                      data &&\n                      data.notifications && (\n                        <div className='flex flex-col'>\n                          {data.notifications.map((item, index) => {\n                            switch (item.messageType) {\n                              case MessageType.REACTION_ADD:\n                                const reaction = item as AccumulatedReaction;\n                                const cast = populateTweetUsers(\n                                  data.tweetsMap[reaction.castId],\n                                  data.usersMap\n                                );\n                                return (\n                                  <div\n                                    key={index}\n                                    className='accent-tab sm:hover-card flex border-b border-light-border px-3 py-2 dark:border-dark-border'\n                                  >\n                                    <HeroIcon\n                                      className='ml-4 mr-6 mt-3 h-6 w-6 flex-shrink-0 flex-grow-0'\n                                      iconName={\n                                        reaction.reactionType ===\n                                          ReactionType.LIKE\n                                          ? 'HeartIcon'\n                                          : 'ArrowPathRoundedSquareIcon'\n                                      }\n                                    />\n                                    <div className='flex flex-col'>\n                                      <div className='flex gap-1 py-2'>\n                                        {reaction.reactions\n                                          .slice(0, 8)\n                                          .map((reaction) => (\n                                            <UserTooltip\n                                              key={reaction.userId}\n                                              {...data.usersMap[\n                                              reaction.userId\n                                              ]}\n                                            >\n                                              {data.usersMap[\n                                                reaction.userId\n                                              ] && (\n                                                  <UserAvatar\n                                                    src={\n                                                      data.usersMap[\n                                                        reaction.userId\n                                                      ].photoURL\n                                                    }\n                                                    alt={\n                                                      data.usersMap[\n                                                        reaction.userId\n                                                      ].name\n                                                    }\n                                                    username={\n                                                      data.usersMap[\n                                                        reaction.userId\n                                                      ].username\n                                                    }\n                                                    size={32}\n                                                  />\n                                                )}\n                                            </UserTooltip>\n                                          ))}\n                                      </div>\n                                      <div>\n                                        <span className='inline-block hover:underline'>\n                                          {data.usersMap[reaction.userId] && (\n                                            <UserTooltip\n                                              key={reaction.userId}\n                                              {...data.usersMap[\n                                              reaction.userId\n                                              ]}\n                                            >\n                                              <UserName\n                                                name={\n                                                  data.usersMap[reaction.userId]\n                                                    .name\n                                                }\n                                                verified={false}\n                                              />\n                                            </UserTooltip>\n                                          )}\n                                        </span>\n                                        { }{' '}\n                                        {reaction.reactions.length > 1\n                                          ? `and ${reaction.reactions.length\n                                          } other${isPlural(\n                                            reaction.reactions.length\n                                          )\n                                            ? 's'\n                                            : ''\n                                          }`\n                                          : ''}{' '}\n                                        {reaction.reactionType ===\n                                          ReactionType.LIKE\n                                          ? 'liked'\n                                          : 'recasted'}{' '}\n                                        your post\n                                      </div>\n                                      <Link\n                                        href={`/tweet/${cast.id}`}\n                                        className='w-full cursor-pointer break-words text-gray-500 [overflow-wrap:anywhere] hover:brightness-75 dark:hover:brightness-125'\n                                      >\n                                        {splitAndInsert(\n                                          cast.text || '',\n                                          cast.mentions.map(\n                                            (mention) => mention.position\n                                          ),\n                                          cast.mentions.map(\n                                            (mention, index) => (\n                                              <>@{mention.username || ''}</>\n                                            )\n                                          ),\n                                          (s) => (\n                                            <>{s}</>\n                                          )\n                                        )}\n                                      </Link>\n                                    </div>\n                                  </div>\n                                );\n                              case MessageType.LINK_ADD:\n                                const link = item as AccumulatedFollow;\n                                const user = data.usersMap[link.userId];\n                                return (\n                                  <div\n                                    key={index}\n                                    className='accent-tab sm:hover-card flex border-b border-light-border px-3 py-2 dark:border-dark-border'\n                                  >\n                                    <HeroIcon\n                                      className='ml-4 mr-6 mt-3 h-6 w-6 flex-shrink-0 flex-grow-0'\n                                      iconName={'UserPlusIcon'}\n                                    />\n                                    <div className='flex flex-col'>\n                                      <div className='flex gap-1 py-2'>\n                                        {link.follows\n                                          .slice(0, 8)\n                                          .map((follow) => (\n                                            <UserTooltip\n                                              key={follow.userId}\n                                              {...data.usersMap[follow.userId]}\n                                            >\n                                              {data.usersMap[follow.userId] && (\n                                                <UserAvatar\n                                                  src={\n                                                    data.usersMap[follow.userId]\n                                                      .photoURL\n                                                  }\n                                                  alt={\n                                                    data.usersMap[follow.userId]\n                                                      .name\n                                                  }\n                                                  username={\n                                                    data.usersMap[follow.userId]\n                                                      .username\n                                                  }\n                                                  size={32}\n                                                />\n                                              )}\n                                            </UserTooltip>\n                                          ))}\n                                      </div>\n\n                                      <div>\n                                        <span className='inline-block hover:underline'>\n                                          {data.usersMap[item.userId] && (\n                                            <UserTooltip\n                                              key={item.userId}\n                                              {...data.usersMap[item.userId]}\n                                            >\n                                              <UserName\n                                                name={\n                                                  data.usersMap[item.userId]\n                                                    .name\n                                                }\n                                                verified={false}\n                                              />\n                                            </UserTooltip>\n                                          )}\n                                        </span>{' '}\n                                        {link.follows.length > 0\n                                          ? ` and ${link.follows.length} other${isPlural(link.follows.length)\n                                            ? 's'\n                                            : ''\n                                          }`\n                                          : ''}{' '}\n                                        followed you\n                                      </div>\n                                    </div>\n                                  </div>\n                                );\n                              case MessageType.CAST_ADD:\n                                // Reply or mention\n                                if ((item as BasicReply).parentUserId) {\n                                  const reply = item as BasicReply;\n                                  return (\n                                    <div key={index}>\n                                      <TweetView\n                                        {...populateTweetUsers(\n                                          data.tweetsMap[reply.castId],\n                                          data.usersMap\n                                        )}\n                                        user={data.usersMap[reply.userId]}\n                                      />\n                                    </div>\n                                  );\n                                } else {\n                                  // Mention\n                                  const mention = item as BasicMention;\n                                  return (\n                                    <div key={index}>\n                                      <TweetView\n                                        {...populateTweetUsers(\n                                          data.tweetsMap[mention.castId],\n                                          data.usersMap\n                                        )}\n                                        user={data.usersMap[mention.userId]}\n                                      />\n                                    </div>\n                                  );\n                                }\n                            }\n                          })}\n                        </div>\n                      )\n                  )}\n                  {hasMore && (\n                    <LoadMoreSentinel\n                      loadMore={() => {\n                        setSize(size + 1);\n                      }}\n                      isLoading={loading}\n                    ></LoadMoreSentinel>\n                  )}\n                </div>\n              )\n            )}\n          </section>\n          {loading && <Loading className='mt-5' />}\n        </>\n      )}\n    </MainContainer>\n  );\n}\n\nNotificationsPage.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/people.tsx.bak",
    "content": "import { useRouter } from 'next/router';\nimport { motion } from 'framer-motion';\nimport { where } from 'firebase/firestore';\nimport { useAuth } from '@lib/context/auth-context';\nimport { usersCollection } from '@lib/firebase/collections';\nimport { useInfiniteScroll } from '@lib/hooks/useInfiniteScroll';\nimport {\n  PeopleLayout,\n  ProtectedLayout\n} from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { SEO } from '@components/common/seo';\nimport { MainHeader } from '@components/home/main-header';\nimport { MainContainer } from '@components/home/main-container';\nimport { UserCard } from '@components/user/user-card';\nimport { Loading } from '@components/ui/loading';\nimport { Error } from '@components/ui/error';\nimport { variants } from '@components/aside/aside-trends';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function People(): JSX.Element {\n  const { user } = useAuth();\n\n  const { data, loading, LoadMore } = useInfiniteScroll(\n    usersCollection,\n    [where('id', '!=', user?.id)],\n    { allowNull: true, preserve: true },\n    { marginBottom: 500 }\n  );\n\n  const { back } = useRouter();\n\n  return (\n    <MainContainer>\n      <SEO title='People / Twitter' />\n      <MainHeader useActionButton title='People' action={back} />\n      <section>\n        {loading ? (\n          <Loading className='mt-5' />\n        ) : !data ? (\n          <Error message='Something went wrong' />\n        ) : (\n          <>\n            <motion.div className='mt-0.5' {...variants}>\n              {data?.map((userData) => (\n                <UserCard {...userData} key={userData.id} follow />\n              ))}\n            </motion.div>\n            <LoadMore />\n          </>\n        )}\n      </section>\n    </MainContainer>\n  );\n}\n\nPeople.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <PeopleLayout>{page}</PeopleLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/settings/index.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { useRouter } from 'next/router';\nimport { type ReactElement, type ReactNode } from 'react';\nimport { MenuLinkProps, MenuRow } from '../../components/ui/menu-row';\nimport { useAuth } from '../../lib/context/auth-context';\n\nconst menuLinks: MenuLinkProps[] = [\n  {\n    href: '/settings/manage-signers',\n    description: 'Manage the keypairs that have access to your account',\n    title: 'Manage Signers',\n    iconName: 'KeyIcon'\n  }\n];\n\nexport default function Settings(): JSX.Element {\n  const { user, userNotifications } = useAuth();\n\n  const { back } = useRouter();\n\n  return (\n    <MainContainer>\n      <SEO title={`Settings / Opencast`} />\n      <MainHeader\n        useMobileSidebar\n        title='Settings'\n        useActionButton\n        action={back}\n      ></MainHeader>\n      {!user?.keyPair ? (\n        <div>You're not logged in.</div>\n      ) : (\n        <div>\n          <section>\n            {menuLinks.map((item) => (\n              <MenuRow {...item} key={item.href}></MenuRow>\n            ))}\n          </section>\n        </div>\n      )}\n    </MainContainer>\n  );\n}\n\nSettings.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/settings/manage-signers/[pubKey].tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { Message } from '@farcaster/hub-web';\nimport cn from 'clsx';\nimport { useRouter } from 'next/router';\nimport { useRef, useState, type ReactElement, type ReactNode } from 'react';\nimport useSWR from 'swr';\nimport { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';\nimport { TweetFeed } from '../../../components/feed/tweet-feed';\nimport { Error } from '../../../components/ui/error';\nimport { HeroIcon } from '../../../components/ui/hero-icon';\nimport { Loading } from '../../../components/ui/loading';\nimport { MenuRow } from '../../../components/ui/menu-row';\nimport { SegmentedNavLink } from '../../../components/ui/segmented-nav-link';\nimport { KEY_REGISTRY } from '../../../contracts';\nimport { useAuth } from '../../../lib/context/auth-context';\nimport { formatDate, formatNumber } from '../../../lib/date';\nimport {\n  batchSubmitHubMessages,\n  makeMessage\n} from '../../../lib/farcaster/utils';\nimport { fetchJSON } from '../../../lib/fetch';\nimport useConnectedWalletFid from '../../../lib/hooks/useConnectedWalletFid';\nimport {\n  MessagesArchive,\n  MessagesArchiveResponse,\n  SignerDetail,\n  SignerResponse\n} from '../../../lib/types/signer';\nimport { truncateAddress } from '../../../lib/utils';\nimport { CautionWarn } from '../../../components/ui/caution-warn';\n\nfunction getSignerDescription(signer: SignerDetail) {\n  return `${formatNumber(\n    signer.messageCount\n  )} messages • Last used ${formatDate(\n    new Date(signer.lastMessageTimestamp),\n    'tweet'\n  )}`;\n\n  //• Created ${formatDate(new Date(signer.createdAtTimestamp), 'tweet')}\n}\n\nexport default function SignerDetailPage(): JSX.Element {\n  const { user } = useAuth();\n  const { data: fid } = useConnectedWalletFid();\n\n  const {\n    query: { pubKey },\n    back\n  } = useRouter();\n\n  const { data: signer, isValidating: loading } = useSWR(\n    `/api/user/${user?.id}/signers/${pubKey}`,\n    async (url: string) => (await fetchJSON<SignerResponse>(url)).result,\n    {\n      revalidateOnFocus: false\n    }\n  );\n\n  const inputFile = useRef<HTMLInputElement>(null);\n\n  const [isBackupLoading, setIsBackupLoading] = useState(false);\n  const [isSignAndBroadcastLoading, setIsSignAndBroadcastLoading] =\n    useState(false);\n  const [isDeleteLoading, setIsDeleteLoading] = useState(false);\n\n  const [importedData, setImportedData] = useState<MessagesArchive | null>(\n    null\n  );\n\n  const {\n    writeContract: removeKey,\n    data: removeKeyHash,\n    isPending: removeKeySignPending,\n    isSuccess: removeKeySignSuccess\n  } = useWriteContract();\n\n  const {\n    data: removeKeyTxReceipt,\n    isSuccess: isRemoveKeyTxSuccess,\n    isLoading: isRemoveKeyTxLoading\n  } = useWaitForTransactionReceipt({ hash: removeKeyHash });\n\n  const handleBackup = async () => {\n    setIsBackupLoading(true);\n    try {\n      const res = await fetchJSON<MessagesArchiveResponse>(\n        `/api/user/${user?.id}/signers/${pubKey}/backup`\n      ).catch((err) => console.error(err));\n      if (!res) return;\n      const result = res.result;\n      const blob = new Blob([JSON.stringify(result)], {\n        type: 'application/json'\n      });\n      const url = URL.createObjectURL(blob);\n      const link = document.createElement('a');\n      link.href = url;\n      link.setAttribute('download', `opencast-backup-${pubKey}.json`);\n      document.body.appendChild(link);\n      link.click();\n      link.remove();\n    } catch (error) {}\n\n    setIsBackupLoading(false);\n  };\n\n  const handleSignAndRebroadcast = async () => {\n    setIsSignAndBroadcastLoading(true);\n\n    try {\n      const signedMessages = await Promise.all(\n        importedData!.messages.map(async (messageJson) => {\n          const message = Message.fromJSON(messageJson);\n          const newMessage = await makeMessage(message.data!);\n          return newMessage;\n        })\n      );\n\n      console.log(signedMessages.map((m) => Message.toJSON(m!)));\n\n      // Submit messages\n      // TODO: See if this works\n      await batchSubmitHubMessages(\n        signedMessages.filter((m) => m !== null) as Message[]\n      );\n    } catch (error) {\n      console.error(error);\n    }\n\n    setIsSignAndBroadcastLoading(false);\n  };\n\n  const handleDelete = async () => {\n    if (!signer) return;\n\n    removeKey?.({\n      ...KEY_REGISTRY,\n      chainId: 10,\n      functionName: 'remove',\n      args: [signer.pubKey]\n    });\n  };\n\n  const matchesCurrentSigner = (pubKey: string) =>\n    user?.keyPair?.publicKey.toLowerCase() === pubKey.toLowerCase();\n\n  return (\n    <MainContainer>\n      <SEO title={`Manage Signer / Opencast`} />\n      <MainHeader\n        useMobileSidebar\n        title={\n          signer\n            ? signer.name || truncateAddress(signer.pubKey)\n            : 'Manage Signer'\n        }\n        description={signer ? getSignerDescription(signer) : ''}\n        useActionButton\n        action={back}\n      >\n        {signer ? (\n          <div>\n            <div></div>\n          </div>\n        ) : (\n          ''\n        )}\n      </MainHeader>\n      <section>\n        {!user?.keyPair ? (\n          <div>You're not logged in.</div>\n        ) : loading ? (\n          <Loading />\n        ) : signer ? (\n          <div>\n            <CautionWarn />\n            <input\n              type='file'\n              id='file'\n              ref={inputFile}\n              style={{ display: 'none' }}\n              accept='application/json'\n              onChange={(e) => {\n                const files = e.target?.files;\n                if (files && files[0]) {\n                  const file = files[0];\n                  var reader = new FileReader();\n                  reader.readAsText(file, 'UTF-8');\n                  reader.onload = function (evt) {\n                    if (evt.target?.result) {\n                      console.log(evt.target.result);\n                      if (typeof evt.target.result === 'string') {\n                        const json = JSON.parse(evt.target.result);\n                        console.log(json);\n                        setImportedData(json);\n                      }\n\n                      //\n                    }\n                  };\n                  reader.onerror = function (evt) {\n                    console.error(`Error loading file`);\n                  };\n                }\n              }}\n            />\n            <MenuRow\n              title='Backup'\n              description='Create an archive of all messages signed by this signer'\n              onClick={handleBackup}\n              iconName='ArchiveBoxArrowDownIcon'\n              isLoading={isBackupLoading}\n            />\n            {matchesCurrentSigner(signer.pubKey) && (\n              <>\n                <MenuRow\n                  title='Import'\n                  description='Import an archive of messages to be re-signed and broadcasted by this signer'\n                  onClick={() => inputFile.current?.click()}\n                  iconName='PencilIcon'\n                />\n                {importedData && (\n                  <div className='flex'>\n                    <div className='w-16 border-b'></div>\n                    <div className='w-full border-l'>\n                      <MenuRow\n                        title='Sign & Rebroadcast'\n                        description={`Sign & rebroadcast ${\n                          importedData.messages.length\n                        } messages by ${\n                          importedData.signer.name ||\n                          truncateAddress(importedData.signer.pubKey)\n                        }`}\n                        onClick={handleSignAndRebroadcast}\n                        iconName='SignalIcon'\n                        isLoading={isSignAndBroadcastLoading}\n                      />\n                    </div>\n                  </div>\n                )}\n              </>\n            )}\n\n            {fid !== undefined && fid.toString() === user.id ? (\n              <MenuRow\n                title='Delete & revoke all'\n                description='Remove the signer and revoke all messages signed by this signer'\n                onClick={handleDelete}\n                iconName='TrashIcon'\n                variant='destructive'\n                isLoading={\n                  isDeleteLoading ||\n                  removeKeySignPending ||\n                  (removeKeyHash != null && isRemoveKeyTxLoading)\n                }\n              />\n            ) : (\n              <div\n                className='accent-tab relative \n               flex cursor-pointer flex-col gap-0.5 border-b border-light-border dark:border-dark-border lg:px-6 lg:py-4'\n              >\n                <div\n                  className={cn(\n                    'flex items-center text-light-secondary dark:text-dark-secondary'\n                  )}\n                >\n                  <div className={cn('mr-4 overflow-hidden')}>\n                    <HeroIcon\n                      className={cn('h-6 w-6')}\n                      iconName={'TrashIcon'}\n                    />\n                  </div>\n                  <div>\n                    <p className='font-bold '>Wallet not connected</p>\n                    <p className='text-sm'>\n                      Connect wallet in the sidebar to remove signer\n                    </p>\n                  </div>\n                </div>\n              </div>\n            )}\n            <div\n              className='hover-animation flex justify-between overflow-y-auto\n    border-b border-light-border dark:border-dark-border'\n            >\n              {[\n                signer.castAddCount + signer.castRemoveCount > 0\n                  ? {\n                      name: `Casts (+${signer.castAddCount} -${signer.castRemoveCount})`,\n                      value: 'casts'\n                    }\n                  : undefined,\n                signer.reactionAddCount + signer.reactionRemoveCount > 0\n                  ? {\n                      name: `Reactions (+${signer.reactionAddCount} -${signer.reactionRemoveCount})`,\n                      value: 'reactions'\n                    }\n                  : undefined,\n                signer.linkAddCount + signer.linkRemoveCount > 0\n                  ? {\n                      name: `Links (+${signer.linkAddCount} -${signer.linkRemoveCount})`,\n                      value: 'links'\n                    }\n                  : undefined,\n                signer.verificationAddEthAddressCount +\n                  signer.verificationRemoveCount >\n                0\n                  ? {\n                      name: `Verifications (+${signer.verificationAddEthAddressCount} -${signer.verificationRemoveCount})`,\n                      value: 'verifications'\n                    }\n                  : undefined\n              ]\n                .filter((i) => i !== undefined)\n                .map((item) => (\n                  <SegmentedNavLink\n                    name={item!.name}\n                    key={item!.value}\n                    isActive={item!.value === 'casts'}\n                    onClick={() => {}}\n                  />\n                ))}\n            </div>\n            <TweetFeed\n              apiEndpoint={`/api/user/${user?.id}/signers/${pubKey}/casts?fid=${user.id}`}\n              feedOrdering='latest'\n            />\n          </div>\n        ) : (\n          <Error />\n        )}\n      </section>\n    </MainContainer>\n  );\n}\n\nSignerDetailPage.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/settings/manage-signers/index.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { useRouter } from 'next/router';\nimport { type ReactElement, type ReactNode } from 'react';\nimport useSWR from 'swr';\nimport { Error } from '../../../components/ui/error';\nimport { Loading } from '../../../components/ui/loading';\nimport { MenuRow } from '../../../components/ui/menu-row';\nimport { useAuth } from '../../../lib/context/auth-context';\nimport { formatDate, formatNumber } from '../../../lib/date';\nimport { fetchJSON } from '../../../lib/fetch';\nimport { SignersResponse } from '../../../lib/types/signer';\nimport { truncateAddress } from '../../../lib/utils';\nimport { CautionWarn } from '../../../components/ui/caution-warn';\n\nexport default function ManageSigners(): JSX.Element {\n  const { user } = useAuth();\n\n  const { back } = useRouter();\n\n  const { data: signers, isValidating: loading } = useSWR(\n    `/api/user/${user?.id}/signers`,\n    async (url: string) => (await fetchJSON<SignersResponse>(url)).result,\n    {\n      revalidateOnFocus: false\n    }\n  );\n\n  const matchesCurrentSigner = (pubKey: string) =>\n    user?.keyPair?.publicKey.toLowerCase() === pubKey.toLowerCase();\n\n  return (\n    <MainContainer>\n      <SEO title={`Manage Signers / Opencast`} />\n      <MainHeader\n        useMobileSidebar\n        title='Manage Signers'\n        useActionButton\n        action={back}\n      ></MainHeader>\n      <section>\n        {!user?.keyPair ? (\n          <div>You're not logged in.</div>\n        ) : loading ? (\n          <Loading />\n        ) : signers ? (\n          <div>\n            <CautionWarn></CautionWarn>\n            {\n              // Show current signer first\n              [\n                signers.find((s) => matchesCurrentSigner(s.pubKey))!,\n                ...signers.filter((s) => !matchesCurrentSigner(s.pubKey))\n              ].map((signer) => (\n                <div\n                  key={signer.pubKey}\n                  title={\n                    matchesCurrentSigner(signer.pubKey)\n                      ? 'Signer currently used by this app'\n                      : undefined\n                  }\n                >\n                  <MenuRow\n                    href={`/settings/manage-signers/${signer.pubKey}`}\n                    title={`${signer.name || truncateAddress(signer.pubKey)}`}\n                    description={`${formatNumber(\n                      signer.messageCount\n                    )} messages • Last used ${formatDate(\n                      new Date(signer.lastMessageTimestamp),\n                      'tweet'\n                    )}`}\n                    /**\n                 • Created ${formatDate(\n                  new Date(signer.createdAtTimestamp),\n                  'tweet'\n                )}\n                 */\n                    iconName='KeyIcon'\n                    variant={\n                      matchesCurrentSigner(signer.pubKey)\n                        ? 'primary'\n                        : undefined\n                    }\n                  ></MenuRow>\n                </div>\n              ))\n            }\n          </div>\n        ) : (\n          <Error />\n        )}\n      </section>\n    </MainContainer>\n  );\n}\n\nManageSigners.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/topic/index.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { Input } from '@components/input/input';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { useWindow } from '@lib/context/window-context';\nimport { useRouter } from 'next/router';\nimport { useEffect, useState, type ReactElement, type ReactNode } from 'react';\nimport useSWR from 'swr';\nimport { TweetFeed } from '../../components/feed/tweet-feed';\nimport { FeedOrderingSelector } from '../../components/ui/feed-ordering-selector';\nimport { fetchJSON } from '../../lib/fetch';\nimport { FeedOrderingType } from '../../lib/types/feed';\nimport { TopicResponse } from '../../lib/types/topic';\nimport { useAuth } from '../../lib/context/auth-context';\n\nexport default function TopicPage(): JSX.Element {\n  // Debounce\n  const [enabled, setEnabled] = useState(false);\n  useEffect(() => {\n    setEnabled(true);\n  }, []);\n\n  const { user } = useAuth();\n\n  const {\n    query: { url: topicUrlParam }\n  } = useRouter();\n\n  const topicUrl = topicUrlParam as string;\n\n  const { data: topic, isValidating: loadingTopic } = useSWR(\n    topicUrl ? `/api/topic?url=${encodeURIComponent(topicUrl)}` : null,\n    async (url) => {\n      const res = await fetchJSON<TopicResponse>(url);\n      return res.result;\n    },\n    { revalidateOnFocus: false }\n  );\n\n  const { isMobile } = useWindow();\n\n  const [feedOrdering, setFeedOrdering] = useState<FeedOrderingType>('latest');\n\n  return (\n    <MainContainer>\n      {\n        <SEO\n          title={`${\n            loadingTopic ? 'Loading' : topic?.name || 'Topic not found'\n          } / Opencast`}\n        />\n      }\n      <MainHeader\n        useMobileSidebar\n        title={topic?.name}\n        imageUrl={topic?.image}\n        description={topic?.description}\n        className='flex items-center justify-between'\n      ></MainHeader>\n      <FeedOrderingSelector {...{ feedOrdering, setFeedOrdering }} />\n      {!isMobile && user?.keyPair && <Input parentUrl={topicUrl} />}\n      <TweetFeed\n        apiEndpoint={`/api/feed?topic_url=${encodeURIComponent(topicUrl)}`}\n        feedOrdering={feedOrdering}\n      />\n    </MainContainer>\n  );\n}\n\nTopicPage.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/trends.tsx",
    "content": "import { useRouter } from 'next/router';\nimport {\n  TrendsLayout,\n  ProtectedLayout\n} from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { SEO } from '@components/common/seo';\nimport { MainHeader } from '@components/home/main-header';\nimport { MainContainer } from '@components/home/main-container';\nimport { AsideTrends } from '@components/aside/trends';\nimport { Button } from '@components/ui/button';\nimport { ToolTip } from '@components/ui/tooltip';\nimport { HeroIcon } from '@components/ui/hero-icon';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function Trends(): JSX.Element {\n  const { back } = useRouter();\n\n  return (\n    <MainContainer>\n      <SEO title='Trends / Opencast' />\n      <MainHeader useActionButton title='Trending topics' action={back}>\n        <Button\n          className='dark-bg-tab group relative ml-auto cursor-not-allowed p-2 hover:bg-light-primary/10\n                     active:bg-light-primary/20 dark:hover:bg-dark-primary/10 dark:active:bg-dark-primary/20'\n        >\n          <HeroIcon className='h-5 w-5' iconName='Cog8ToothIcon' />\n          <ToolTip tip='Settings' />\n        </Button>\n      </MainHeader>\n      <AsideTrends inTrendsPage />\n    </MainContainer>\n  );\n}\n\nTrends.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <TrendsLayout>{page}</TrendsLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/tweet/[id].tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { MainContainer } from '@components/home/main-container';\nimport { MainHeader } from '@components/home/main-header';\nimport { HomeLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { Error } from '@components/ui/error';\nimport { Loading } from '@components/ui/loading';\nimport { ViewParentTweet } from '@components/view/view-parent-tweet';\nimport { ViewTweet } from '@components/view/view-tweet';\nimport { isPlural } from '@lib/utils';\nimport { useRouter } from 'next/router';\nimport {\n  ReactElement,\n  ReactNode,\n  useEffect,\n  useMemo,\n  useRef,\n  useState\n} from 'react';\nimport useSWR from 'swr';\nimport { Tweet } from '../../components/tweet/tweet';\nimport { fetchJSON } from '../../lib/fetch';\nimport { useInfiniteScroll } from '../../lib/hooks/useInfiniteScroll';\nimport { TweetResponse, populateTweetUsers } from '../../lib/types/tweet';\n\nexport default function TweetId(): JSX.Element {\n  const {\n    query: { id },\n    back\n  } = useRouter();\n\n  const [enabled, setEnabled] = useState(false);\n  useEffect(() => {\n    setEnabled(true);\n  }, []);\n\n  const { data: tweetData, isValidating: tweetLoading } = useSWR(\n    `/api/tweet/${id}`,\n    async (url) => (await fetchJSON<TweetResponse>(url)).result\n  );\n\n  const {\n    data: repliesData,\n    loading: repliesLoading,\n    LoadMore\n  } = useInfiniteScroll(\n    (pageParam) =>\n      `/api/tweet/${id}/replies?limit=10${\n        pageParam ? `&cursor=${pageParam}` : ''\n      }`,\n    { queryKey: ['replies', id], enabled, refetchOnFocus: false }\n  );\n\n  const viewTweetRef = useRef<HTMLElement>(null);\n\n  const { text, images } = tweetData ?? {};\n\n  const imagesLength = images?.length ?? 0;\n  const parentId = tweetData?.parent?.id;\n\n  const pageTitle = tweetData\n    ? `${tweetData.users[tweetData.createdBy]?.name} on Opencast: \"${\n        text ?? ''\n      }${\n        images ? ` (${imagesLength} image${isPlural(imagesLength)})` : ''\n      }\" / Opencast`\n    : null;\n\n  const tweetWithPopulatedUsers = useMemo(() => {\n    if (!tweetData) return;\n    return populateTweetUsers(tweetData, tweetData.users);\n  }, [tweetData]);\n\n  return (\n    <MainContainer className='!pb-[1280px]'>\n      <MainHeader\n        useActionButton\n        title={parentId ? 'Thread' : 'Cast'}\n        action={back}\n      />\n      <section>\n        {tweetLoading && !tweetData ? (\n          <Loading className='mt-5' />\n        ) : !(tweetWithPopulatedUsers && tweetData) ? (\n          <>\n            <SEO title='Cast not found / Opencast' />\n            <Error message='Cast not found' />\n          </>\n        ) : (\n          <>\n            {pageTitle && <SEO title={pageTitle} />}\n            {parentId && (\n              <ViewParentTweet\n                parentId={parentId}\n                viewTweetRef={viewTweetRef}\n              />\n            )}\n            <ViewTweet\n              viewTweetRef={viewTweetRef}\n              {...tweetWithPopulatedUsers}\n              usersMap={tweetData.users}\n              user={tweetData.users[tweetData.createdBy]}\n            />\n            {tweetData &&\n              (repliesLoading ? (\n                <Loading className='mt-5' />\n              ) : !repliesData ? (\n                <div>No replies</div>\n              ) : (\n                <div>\n                  {repliesData.pages.map((page) => {\n                    if (!page) return;\n                    const { tweets, users } = page;\n                    return tweets.map((tweet) => {\n                      if (!users[tweet.createdBy]) {\n                        return <></>;\n                      }\n\n                      return (\n                        <Tweet\n                          {...populateTweetUsers(tweet, users)}\n                          usersMap={users}\n                          user={users[tweet.createdBy]}\n                          key={tweet.id}\n                        />\n                      );\n                    });\n                  })}\n                  <LoadMore />\n                </div>\n              ))}\n          </>\n        )}\n      </section>\n    </MainContainer>\n  );\n}\n\nTweetId.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <HomeLayout>{page}</HomeLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/user/[id]/followers.tsx",
    "content": "import { UserLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { UserDataLayout } from '@components/layout/user-data-layout';\nimport { UserFollowLayout } from '@components/layout/user-follow-layout';\nimport { UserFollow } from '@components/user/user-follow';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function UserFollowers(): JSX.Element {\n  return <UserFollow type='followers' />;\n}\n\nUserFollowers.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <UserLayout>\n        <UserDataLayout>\n          <UserFollowLayout>{page}</UserFollowLayout>\n        </UserDataLayout>\n      </UserLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/user/[id]/following.tsx",
    "content": "import { UserLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { UserDataLayout } from '@components/layout/user-data-layout';\nimport { UserFollowLayout } from '@components/layout/user-follow-layout';\nimport { UserFollow } from '@components/user/user-follow';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function UserFollowing(): JSX.Element {\n  return <UserFollow type='following' />;\n}\n\nUserFollowing.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <UserLayout>\n        <UserDataLayout>\n          <UserFollowLayout>{page}</UserFollowLayout>\n        </UserDataLayout>\n      </UserLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/user/[id]/index.tsx",
    "content": "import { ProtectedLayout, UserLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { UserDataLayout } from '@components/layout/user-data-layout';\nimport { UserHomeLayout } from '@components/layout/user-home-layout';\nimport { StatsEmpty } from '@components/tweet/stats-empty';\nimport { Tweet } from '@components/tweet/tweet';\nimport { Loading } from '@components/ui/loading';\nimport { useUser } from '@lib/context/user-context';\nimport { useEffect, useState, type ReactElement, type ReactNode } from 'react';\nimport { useInfiniteScroll } from '../../../lib/hooks/useInfiniteScroll';\nimport { populateTweetUsers } from '../../../lib/types/tweet';\n\nexport default function UserTweets(): JSX.Element {\n  const { user } = useUser();\n\n  // Debounce\n  const [enabled, setEnabled] = useState(false);\n  useEffect(() => {\n    setEnabled(true);\n  }, []);\n\n  const { id, username } = user ?? {};\n  const {\n    data: ownerTweets,\n    loading: ownerLoading,\n    LoadMore\n  } = useInfiniteScroll(\n    (pageParam) =>\n      `/api/user/${id}/tweets?limit=10${pageParam ? `&cursor=${pageParam}` : ''\n      }`,\n    { marginBottom: 20, queryKey: ['user', id], enabled }\n  );\n\n  // const { data: peopleTweets, loading: peopleLoading } = useCollection(\n  //   query(\n  //     tweetsCollection,\n  //     where('createdBy', '!=', id),\n  //     where('userRetweets', 'array-contains', id)\n  //   ),\n  //   { includeUser: true, allowNull: true }\n  // );\n\n  // const mergedTweets = mergeData(true, ownerTweets, peopleTweets);\n  const mergedTweets = ownerTweets;\n\n  return (\n    <section>\n      {ownerLoading ? (\n        <Loading className='mt-5' />\n      ) : !mergedTweets ? (\n        <StatsEmpty\n          title={`@${username as string} hasn't tweeted`}\n          description='When they do, their Tweets will show up here.'\n        />\n      ) : (\n        <div>\n          {mergedTweets.pages.map((page) => {\n            if (!page) return;\n            const { tweets, users } = page;\n            return tweets.map((tweet) => {\n              if (!users[tweet.createdBy]) {\n                return <></>;\n              }\n\n              return (\n                <Tweet\n                  {...populateTweetUsers(tweet, users)}\n                  user={users[tweet.createdBy]}\n                  usersMap={users}\n                  key={tweet.id}\n                />\n              );\n            });\n          })}\n          <LoadMore />\n        </div>\n      )}\n    </section>\n  );\n}\n\nUserTweets.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <UserLayout>\n        <UserDataLayout>\n          <UserHomeLayout>{page}</UserHomeLayout>\n        </UserDataLayout>\n      </UserLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/user/[id]/likes.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { ProtectedLayout, UserLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { UserDataLayout } from '@components/layout/user-data-layout';\nimport { UserHomeLayout } from '@components/layout/user-home-layout';\nimport { StatsEmpty } from '@components/tweet/stats-empty';\nimport { Tweet } from '@components/tweet/tweet';\nimport { Loading } from '@components/ui/loading';\nimport { useUser } from '@lib/context/user-context';\nimport { useEffect, useState, type ReactElement, type ReactNode } from 'react';\nimport { useInfiniteScroll } from '../../../lib/hooks/useInfiniteScroll';\nimport { populateTweetUsers } from '../../../lib/types/tweet';\n\nexport default function UserLikes(): JSX.Element {\n  const { user } = useUser();\n\n  // Debounce\n  const [enabled, setEnabled] = useState(false);\n  useEffect(() => {\n    setEnabled(true);\n  }, []);\n\n  const { id, username, name } = user ?? {};\n  const { data, loading, LoadMore } = useInfiniteScroll(\n    (pageParam) =>\n      `/api/user/${id}/likes?limit=10${\n        pageParam ? `&cursor=${pageParam}` : ''\n      }`,\n    { marginBottom: 20, queryKey: ['user', 'likes', id], enabled }\n  );\n\n  return (\n    <section>\n      <SEO\n        title={`Tweets liked by ${name as string} (@${\n          username as string\n        }) / Opencast`}\n      />\n      {loading ? (\n        <Loading className='mt-5' />\n      ) : !data ? (\n        <StatsEmpty\n          title={`@${username as string} hasn't liked any Tweets`}\n          description='When they do, those Tweets will show up here.'\n        />\n      ) : (\n        <div>\n          {/* {pinnedData && (\n            <Tweet pinned {...pinnedData} key={`pinned-${pinnedData.id}`} />\n          )}\n          {mergedTweets.map((tweet) => (\n            <Tweet {...tweet} profile={user} key={tweet.id} />\n          ))} */}\n          {data.pages.map((page) => {\n            if (!page) return;\n            const { tweets, users } = page;\n            return tweets.map((tweet) => {\n              if (!users[tweet.createdBy]) {\n                return <></>;\n              }\n\n              return (\n                <Tweet\n                  {...populateTweetUsers(tweet, users)}\n                  user={users[tweet.createdBy]}\n                  key={tweet.id}\n                />\n              );\n            });\n          })}\n          <LoadMore />\n          {/* </AnimatePresence> */}\n        </div>\n      )}\n    </section>\n  );\n}\n\nUserLikes.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <UserLayout>\n        <UserDataLayout>\n          <UserHomeLayout>{page}</UserHomeLayout>\n        </UserDataLayout>\n      </UserLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/user/[id]/media.tsx.bak",
    "content": "import { AnimatePresence } from 'framer-motion';\nimport { query, where } from 'firebase/firestore';\nimport { useCollection } from '@lib/hooks/useCollection';\nimport { tweetsCollection } from '@lib/firebase/collections';\nimport { useUser } from '@lib/context/user-context';\nimport { mergeData } from '@lib/merge';\nimport { UserLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { SEO } from '@components/common/seo';\nimport { UserDataLayout } from '@components/layout/user-data-layout';\nimport { UserHomeLayout } from '@components/layout/user-home-layout';\nimport { Tweet } from '@components/tweet/tweet';\nimport { Loading } from '@components/ui/loading';\nimport { StatsEmpty } from '@components/tweet/stats-empty';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function UserMedia(): JSX.Element {\n  const { user } = useUser();\n\n  const { id, name, username } = user ?? {};\n\n  const { data, loading } = useCollection(\n    query(\n      tweetsCollection,\n      where('createdBy', '==', id),\n      where('images', '!=', null)\n    ),\n    { includeUser: true, allowNull: true }\n  );\n\n  const sortedTweets = mergeData(true, data);\n\n  return (\n    <section>\n      <SEO\n        title={`Media Tweets by ${name as string} (@${\n          username as string\n        }) / Twitter`}\n      />\n      {loading ? (\n        <Loading className='mt-5' />\n      ) : !sortedTweets ? (\n        <StatsEmpty\n          title={`@${username as string} hasn't Tweeted Media`}\n          description='Once they do, those Tweets will show up here.'\n          imageData={{ src: '/assets/no-media.png', alt: 'No media' }}\n        />\n      ) : (\n        <AnimatePresence mode='popLayout'>\n          {sortedTweets.map((tweet) => (\n            <Tweet {...tweet} key={tweet.id} />\n          ))}\n        </AnimatePresence>\n      )}\n    </section>\n  );\n}\n\nUserMedia.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <UserLayout>\n        <UserDataLayout>\n          <UserHomeLayout>{page}</UserHomeLayout>\n        </UserDataLayout>\n      </UserLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/user/[id]/with_replies.tsx",
    "content": "import { SEO } from '@components/common/seo';\nimport { ProtectedLayout, UserLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { UserDataLayout } from '@components/layout/user-data-layout';\nimport { UserHomeLayout } from '@components/layout/user-home-layout';\nimport { StatsEmpty } from '@components/tweet/stats-empty';\nimport { LoadedParents } from '@components/tweet/tweet-with-parent';\nimport { Loading } from '@components/ui/loading';\nimport { useUser } from '@lib/context/user-context';\nimport { useEffect, useState, type ReactElement, type ReactNode } from 'react';\nimport { Tweet } from '../../../components/tweet/tweet';\nimport { TweetParent } from '../../../components/tweet/tweet-parent';\nimport { useInfiniteScroll } from '../../../lib/hooks/useInfiniteScroll';\nimport { populateTweetUsers } from '../../../lib/types/tweet';\n\nexport default function UserWithReplies(): JSX.Element {\n  const { user } = useUser();\n\n  // Debounce\n  const [enabled, setEnabled] = useState(false);\n  useEffect(() => {\n    setEnabled(true);\n  }, []);\n\n  const { id, username, name } = user ?? {};\n  const { data, loading, LoadMore } = useInfiniteScroll(\n    (pageParam) =>\n      `/api/user/${id}/tweets?limit=10&replies=true${\n        pageParam ? `&cursor=${pageParam}` : ''\n      }`,\n    {\n      marginBottom: 20,\n      queryKey: ['user', 'likes', id],\n      enabled,\n      refetchOnFocus: false\n    }\n  );\n\n  const [loadedParents, setLoadedParents] = useState<LoadedParents>([]);\n\n  const addParentId = (parentId: string, targetChildId: string): void =>\n    setLoadedParents((prevLoadedParents) =>\n      prevLoadedParents.some((item) => item.parentId === parentId)\n        ? prevLoadedParents\n        : [...prevLoadedParents, { parentId, childId: targetChildId }]\n    );\n\n  return (\n    <section>\n      <SEO\n        title={`Tweets with replies by ${name as string} (@${\n          username as string\n        }) / Opencast`}\n      />\n      {loading ? (\n        <Loading className='mt-5' />\n      ) : !data ? (\n        <StatsEmpty\n          title={`@${username as string} hasn't tweeted`}\n          description='When they do, their Tweets will show up here.'\n        />\n      ) : (\n        <div>\n          {data.pages.map((page) => {\n            if (!page) return;\n            const { tweets, users } = page;\n            return tweets.map((tweet) => {\n              if (!users[tweet.createdBy]) {\n                return <></>;\n              }\n\n              return (\n                <div className='[&>article:nth-child(2)]:-mt-1' key={tweet.id}>\n                  {tweet.parent && (\n                    <TweetParent\n                      parentId={tweet.parent.id}\n                      loadedParents={loadedParents}\n                      addParentId={addParentId}\n                    />\n                  )}\n                  <Tweet\n                    {...populateTweetUsers(tweet, users)}\n                    user={users[tweet.createdBy]}\n                    key={tweet.id}\n                  />\n                </div>\n              );\n            });\n          })}\n          <LoadMore />\n        </div>\n      )}\n    </section>\n  );\n}\n\nUserWithReplies.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <UserLayout>\n        <UserDataLayout>\n          <UserHomeLayout>{page}</UserHomeLayout>\n        </UserDataLayout>\n      </UserLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/pages/user/[id]/with_replies.tsx.bak",
    "content": "import { AnimatePresence } from 'framer-motion';\nimport { doc, query, where, orderBy } from 'firebase/firestore';\nimport { useCollection } from '@lib/hooks/useCollection';\nimport { useDocument } from '@lib/hooks/useDocument';\nimport { tweetsCollection } from '@lib/firebase/collections';\nimport { useUser } from '@lib/context/user-context';\nimport { UserLayout, ProtectedLayout } from '@components/layout/common-layout';\nimport { MainLayout } from '@components/layout/main-layout';\nimport { SEO } from '@components/common/seo';\nimport { UserDataLayout } from '@components/layout/user-data-layout';\nimport { UserHomeLayout } from '@components/layout/user-home-layout';\nimport { Tweet } from '@components/tweet/tweet';\nimport { Loading } from '@components/ui/loading';\nimport { StatsEmpty } from '@components/tweet/stats-empty';\nimport { TweetWithParent } from '@components/tweet/tweet-with-parent';\nimport type { ReactElement, ReactNode } from 'react';\n\nexport default function UserWithReplies(): JSX.Element {\n  const { user } = useUser();\n\n  const { id, name, username, pinnedTweet } = user ?? {};\n\n  const { data: pinnedData } = useDocument(\n    doc(tweetsCollection, pinnedTweet ?? 'null'),\n    {\n      disabled: !pinnedTweet,\n      allowNull: true,\n      includeUser: true\n    }\n  );\n\n  const { data, loading } = useCollection(\n    query(\n      tweetsCollection,\n      where('createdBy', '==', id),\n      orderBy('createdAt', 'desc')\n    ),\n    { includeUser: true, allowNull: true }\n  );\n\n  return (\n    <section>\n      <SEO\n        title={`Tweets with replies by ${name as string} (@${\n          username as string\n        }) / Twitter`}\n      />\n      {loading ? (\n        <Loading className='mt-5' />\n      ) : !data ? (\n        <StatsEmpty\n          title={`@${username as string} hasn't tweeted`}\n          description='When they do, their Tweets will show up here.'\n        />\n      ) : (\n        <AnimatePresence mode='popLayout'>\n          {pinnedData && (\n            <Tweet pinned {...pinnedData} key={`pinned-${pinnedData.id}`} />\n          )}\n          <TweetWithParent data={data} />\n        </AnimatePresence>\n      )}\n    </section>\n  );\n}\n\nUserWithReplies.getLayout = (page: ReactElement): ReactNode => (\n  <ProtectedLayout>\n    <MainLayout>\n      <UserLayout>\n        <UserDataLayout>\n          <UserHomeLayout>{page}</UserHomeLayout>\n        </UserDataLayout>\n      </UserLayout>\n    </MainLayout>\n  </ProtectedLayout>\n);\n"
  },
  {
    "path": "src/styles/fonts.scss",
    "content": "@font-face {\n  font-family: TwitterChirpExtendedHeavy;\n  src: url(/fonts/chirp-extended-heavy-web.woff2) format('woff2');\n  src: url(/fonts/chirp-extended-heavy-web.woff) format('woff');\n  font-weight: 700;\n  font-style: 'normal';\n  font-display: 'swap';\n}\n\n@font-face {\n  font-family: TwitterChirp;\n  src: url(/fonts/chirp-regular-web.woff2) format('woff2');\n  src: url(/fonts/chirp-regular-web.woff) format('woff');\n  font-weight: 400;\n  font-style: 'normal';\n  font-display: 'swap';\n}\n\n@font-face {\n  font-family: TwitterChirp;\n  src: url(/fonts/chirp-medium-web.woff2) format('woff2');\n  src: url(/fonts/chirp-medium-web.woff) format('woff');\n  font-weight: 500;\n  font-style: 'normal';\n  font-display: 'swap';\n}\n\n@font-face {\n  font-family: TwitterChirp;\n  src: url(/fonts/chirp-bold-web.woff2) format('woff2');\n  src: url(/fonts/chirp-bold-web.woff) format('woff');\n  font-weight: 700;\n  font-style: 'normal';\n  font-display: 'swap';\n}\n\n@font-face {\n  font-family: TwitterChirp;\n  src: url(/fonts/chirp-heavy-web.woff2) format('woff2');\n  src: url(/fonts/chirp-heavy-web.woff) format('woff');\n  font-weight: 800;\n  font-style: 'normal';\n  font-display: 'swap';\n}\n"
  },
  {
    "path": "src/styles/globals.scss",
    "content": "@use 'fonts';\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --main-background: var(--dark-background);\n    --main-search-background: var(--dark-search-background);\n    --main-sidebar-background: var(--dark-sidebar-background);\n    --main-accent: var(--accent-blue);\n\n    --dark-background: 0 0 0;\n    --dim-background: 22 33 44;\n    --light-background: 255 255 255;\n\n    --dark-search-background: 32 35 39;\n    --dim-search-background: 39 51 64;\n    --light-search-background: 239 243 244;\n\n    --dark-sidebar-background: 22 24 28;\n    --dim-sidebar-background: 30 39 50;\n    --light-sidebar-background: 247 249 249;\n\n    --accent-yellow: 255 213 0;\n    --accent-blue: 29 155 240;\n    --accent-pink: 249 26 130;\n    --accent-purple: 120 87 255;\n    --accent-orange: 255 122 0;\n    --accent-green: 0 184 122;\n  }\n\n  a,\n  input,\n  button,\n  textarea {\n    -webkit-tap-highlight-color: transparent;\n  }\n\n  body {\n    @apply hover-animation bg-main-background font-twitter-chirp text-light-primary dark:text-dark-primary;\n  }\n\n  // Mod Embed border colour\n  *,\n  ::before,\n  ::after {\n    @apply dark:border-dark-border;\n  }\n\n  // Handle broken images\n  img:before {\n    content: ' ';\n    display: block;\n    // position: absolute;\n    height: 100%;\n    width: 100%;\n    @apply dark:bg-dark-secondary;\n    @apply bg-light-secondary;\n  }\n}\n\n@layer components {\n  .hover-animation {\n    @apply transition-colors duration-200;\n  }\n\n  .custom-button {\n    @apply hover-animation rounded-full p-3 disabled:cursor-not-allowed disabled:opacity-50;\n  }\n\n  .custom-underline {\n    @apply hover-animation underline decoration-transparent outline-none transition [text-decoration-thickness:1px] \n           hover:decoration-inherit focus-visible:decoration-inherit;\n  }\n\n  .main-tab {\n    @apply outline-none focus-visible:ring-2 focus-visible:ring-[#878a8c] focus-visible:transition-shadow \n           focus-visible:duration-200 dark:focus-visible:ring-white;\n  }\n\n  .accent-tab {\n    @apply main-tab #{'focus-visible:!ring-main-accent/80'};\n  }\n\n  .accent-bg-tab {\n    @apply focus-visible:bg-main-accent/10;\n  }\n\n  .dark-bg-tab {\n    @apply focus-visible:bg-light-primary/10 dark:focus-visible:bg-dark-primary/10;\n  }\n\n  .blur-picture {\n    @apply hover-animation accent-tab rounded-full transition hover:brightness-75 active:brightness-100;\n  }\n\n  .trim-alt {\n    @apply overflow-hidden text-ellipsis break-all [-webkit-box-orient:vertical] [-webkit-line-clamp:1] [display:-webkit-box];\n  }\n\n  .hover-card {\n    @apply hover:bg-black/[0.03] focus-visible:bg-black/[0.03] dark:hover:bg-white/[0.03]\n           dark:focus-visible:bg-white/[0.03];\n  }\n\n  .menu-container {\n    @apply z-10 rounded-md bg-main-background outline-none \n           [box-shadow:#65778633_0px_0px_15px,_#65778626_0px_0px_3px_1px] \n           dark:[box-shadow:#ffffff33_0px_0px_15px,_#ffffff26_0px_0px_3px_1px];\n  }\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\n\nconst defaultTheme = require('tailwindcss/defaultTheme');\n\nmodule.exports = {\n  darkMode: 'class',\n  content: ['src/pages/**/*.tsx', 'src/components/**/*.tsx'],\n  theme: {\n    screens: {\n      xs: '500px',\n      ...defaultTheme.screens\n    },\n    extend: {\n      fontFamily: {\n        'twitter-chirp': ['TwitterChirp', 'sans-serif'],\n        'twitter-chirp-extended': ['TwitterChirpExtendedHeavy', 'sans-serif']\n      },\n      // prettier-ignore\n      colors: {\n        'main-primary': 'rgb(var(--main-primary) / <alpha-value>)',\n        'main-secondary': 'rgb(var(--main-secondary) / <alpha-value>)',\n        'main-background': 'rgb(var(--main-background) / <alpha-value>)',\n        'main-search-background': 'rgb(var(--main-search-background) / <alpha-value>)',\n        'main-sidebar-background': 'rgb(var(--main-sidebar-background) / <alpha-value>)',\n        'main-accent': 'rgb(var(--main-accent) / <alpha-value>)',\n        'accent-yellow': 'rgb(var(--accent-yellow) / <alpha-value>)',\n        'accent-blue': 'rgb(var(--accent-blue) / <alpha-value>)',\n        'accent-pink': 'rgb(var(--accent-pink) / <alpha-value>)',\n        'accent-purple': 'rgb(var(--accent-purple) / <alpha-value>)',\n        'accent-orange': 'rgb(var(--accent-orange) / <alpha-value>)',\n        'accent-green': 'rgb(var(--accent-green) / <alpha-value>)',\n        'accent-red': '#F4212E',\n        'dark-primary': '#E7E9EA',\n        'dark-secondary': '#71767B',\n        'light-primary': '#0F1419',\n        'light-secondary': '#536471',\n        'dark-border': '#2F3336',\n        'light-border': '#EFF3F4',\n        'dark-line-reply': '#333639',\n        'light-line-reply': '#CFD9DE',\n        'twitter-icon': '#D6D9DB',\n        'image-preview-hover': '#272C30',\n      }\n    }\n  },\n  plugins: [\n    ({ addVariant }) => {\n      addVariant('inner', '& > *');\n    }\n  ]\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2021\", // Setting this to `ES2021` enables native support for `Node v16+`: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping.\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\",\n      \"ES2022\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"@components/*\": [\n        \"components/*\"\n      ],\n      \"@lib/*\": [\n        \"lib/*\"\n      ],\n      \"@styles/*\": [\n        \"styles/*\"\n      ]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"functions\",\n    \"**/*.bak\"\n  ]\n}\n"
  }
]