[
  {
    "path": ".eslintrc.json",
    "content": "{\n\t\"extends\": \"next/core-web-vitals\"\n}\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\n# local env files\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n\n# custom\nsrc/data/ens.js\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) Miguel Piedrafita\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 furnished\nto 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\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# A Next.js-powered frontend for your Mirror blog\n\n> This project mimics the [Mirror](https://mirror.xyz) publication design and pulls data from their APIs, allowing you to self-host your Mirror blog on a custom domain.\n\nYou can view a demo of this project by visiting [m1guelpf.blog](https://m1guelpf.blog), which hosts the [my Mirror publication](https://miguel.mirror.xyz).\n\n## Features\n\n-   [x] Article list\n-   [x] Article page\n-   [x] Code highlighting (using VSCode's rendering engine, allows for custom themes. Currently using `github-light` and `github-dark`, depending on the publication's theme)\n-   [x] Dark mode (when enabled on Mirror)\n-   [x] Patience page (when no articles exist)\n-   [x] Static generation (all pages should load instantly once deployed)\n-   [x] Static re-generation (new articles should appear without re-deploying)\n-   [x] Embeds\n    -   [x] Tweet embeds\n    -   [x] YouTube embeds\n    -   [x] Additional embeds (CodePen, JSBin, Gists, etc., not sure if supported by Mirror already)\n    -   [x] NFT embeds\n    -   [x] Bookmark cards (Open Graph)\n-   [x] Email list support (when enabled on Mirror)\n-   [x] Pull content from Arweave\n-   [x] Write Mirror entry about this project\n\n## Development\n\n-   Clone this repo in a local directory\n-   Install dependencies (`pnpm install`)\n-   Copy the `.env.example` file to `.env.local`, and fill in your mirror subdomain and an RPC URL\n-   Start the server! (`pnpm dev`)\n\n## Deploying to Vercel\n\nYou can deploy this project to Vercel (and load your own publication!) by clicking the button below:\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fm1guelpf%2Fmirror-next&env=NEXT_PUBLIC_AUTHOR_ENS,NEXT_PUBLIC_RPC_URL&envDescription=The%20ENS%20for%20the%20publication%20you%20want%20to%20load%2C%20and%20an%20RPC%20URL.&project-name=mirror-next&repo-name=mirror-next)\n\nOnce it's ready, you should be able to attach your custom domain from the Vercel settings page.\n\n## FAQ\n\n**Is this decentralized?**\n\nKind of. While I'm pulling the entry listing and contents from the Arweave chain directly, the publication details come from Mirror's APIs.\n\n**Why did you make this?**\n\nI like playing with stuff :). I really like Mirror's design, so I decided to create a [Ghost](https://ghost.org) theme \"inspired\" by their design. Once that was finished, I decided to turn that theme into something slightly more useful.\n\n**Who are you?**\n\n:wave: Hi! I'm [Miguel Piedrafita](https://twitter.com/m1guelpf), an 22-year-old maker. You can follow my journey and all the little things I make on the way [on Twitter](https://twitter.com/m1guelpf).\n\n**I have another question**\n\nRead [Building apps with Mirror](https://m1guelpf.blog/post/building-apps-with-mirror), an entry I wrote in my own Mirror publication explaining how this project works, and how you can build your own Mirror apps. If you still have questions after that, [drop me a line on Twitter](https://twitter.com/m1guelpf).\n\n## License\n\nThis project is open-sourced software licensed under the MIT license. See the [License file](LICENSE.md) for more information.\n"
  },
  {
    "path": "next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "next.config.js",
    "content": "import resolveEns from './scripts/resolve-ens.js'\n\nexport default () => {\n\tresolveEns()\n\n\treturn {\n\t\timages: {\n\t\t\tremotePatterns: [{ hostname: 'images.mirror-media.xyz' }],\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"mirror-next\",\n\t\"version\": \"0.1.0\",\n\t\"private\": true,\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"next dev\",\n\t\t\"build\": \"next build\",\n\t\t\"start\": \"next start\"\n\t},\n\t\"dependencies\": {\n\t\t\"@apollo/client\": \"^3.10.4\",\n\t\t\"@heroicons/react\": \"^2.1.3\",\n\t\t\"@tailwindcss/typography\": \"^0.5.13\",\n\t\t\"arweave\": \"^1.15.1\",\n\t\t\"autoprefixer\": \"^10.4.19\",\n\t\t\"fs\": \"^0.0.1-security\",\n\t\t\"graphql\": \"^16.8.1\",\n\t\t\"image-size\": \"^1.1.1\",\n\t\t\"metascraper\": \"^5.45.8\",\n\t\t\"metascraper-author\": \"^5.45.7\",\n\t\t\"metascraper-clearbit\": \"^5.45.7\",\n\t\t\"metascraper-date\": \"^5.45.7\",\n\t\t\"metascraper-description\": \"^5.45.7\",\n\t\t\"metascraper-image\": \"^5.45.7\",\n\t\t\"metascraper-lang\": \"^5.45.7\",\n\t\t\"metascraper-logo\": \"^5.45.7\",\n\t\t\"metascraper-logo-favicon\": \"^5.45.7\",\n\t\t\"metascraper-publisher\": \"^5.45.7\",\n\t\t\"metascraper-title\": \"^5.45.7\",\n\t\t\"metascraper-url\": \"^5.45.7\",\n\t\t\"net\": \"^1.0.2\",\n\t\t\"next\": \"^14.2.3\",\n\t\t\"postcss\": \"^8.4.38\",\n\t\t\"react\": \"18.3.1\",\n\t\t\"react-dom\": \"18.3.1\",\n\t\t\"react-embed\": \"^3.7.0\",\n\t\t\"react-markdown\": \"^9.0.1\",\n\t\t\"react-medium-image-zoom\": \"^4.3.1\",\n\t\t\"rehype-raw\": \"^7.0.0\",\n\t\t\"rehype-sanitize\": \"^6.0.0\",\n\t\t\"rehype-stringify\": \"^10.0.0\",\n\t\t\"remark-parse\": \"^11.0.0\",\n\t\t\"remark-rehype\": \"^11.1.0\",\n\t\t\"remark-stringify\": \"^11.0.0\",\n\t\t\"rss\": \"^1.2.2\",\n\t\t\"shiki\": \"^1.6.0\",\n\t\t\"slug\": \"^9.0.0\",\n\t\t\"strip-markdown\": \"^6.0.0\",\n\t\t\"swr\": \"^2.2.5\",\n\t\t\"tailwindcss\": \"^3.4.3\",\n\t\t\"timeago.js\": \"^4.0.2\",\n\t\t\"tls\": \"^0.0.1\",\n\t\t\"unified\": \"^11.0.4\",\n\t\t\"unist-util-visit\": \"^5.0.0\",\n\t\t\"viem\": \"^2.11.1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"eslint\": \"^9.3.0\",\n\t\t\"prettier\": \"^2.2.1\",\n\t\t\"prettier-plugin-sort-imports-desc\": \"^1.0.0\",\n\t\t\"typescript\": \"5.4.5\"\n\t},\n\t\"prettier\": {\n\t\t\"semi\": false,\n\t\t\"tabWidth\": 4,\n\t\t\"useTabs\": true,\n\t\t\"printWidth\": 120,\n\t\t\"singleQuote\": true,\n\t\t\"arrowParens\": \"avoid\",\n\t\t\"trailingComma\": \"es5\",\n\t\t\"bracketSpacing\": true,\n\t\t\"plugins\": [\n\t\t\t\"prettier-plugin-sort-imports-desc\"\n\t\t]\n\t},\n\t\"pnpm\": {\n\t\t\"patchedDependencies\": {\n\t\t\t\"re2@1.20.11\": \"patches/re2@1.20.11.patch\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "patches/re2@1.20.11.patch",
    "content": "diff --git a/package.json b/package.json\nindex c3bc354aa66946406f62bd7805fc88182039b081..b53fb74d1410881b38b0fcbac2f7ad49a12fe778 100644\n--- a/package.json\n+++ b/package.json\n@@ -28,7 +28,8 @@\n     \"test\": \"node tests/tests.js\",\n     \"ts-test\": \"tsc\",\n     \"save-to-github\": \"save-to-github-cache --artifact build/Release/re2.node\",\n-    \"install\": \"install-from-cache --artifact build/Release/re2.node --host-var RE2_DOWNLOAD_MIRROR --skip-path-var RE2_DOWNLOAD_SKIP_PATH --skip-ver-var RE2_DOWNLOAD_SKIP_VER || node-gyp rebuild\",\n+    \"original-install\": \"install-from-cache --artifact build/Release/re2.node --host-var RE2_DOWNLOAD_MIRROR --skip-path-var RE2_DOWNLOAD_SKIP_PATH --skip-ver-var RE2_DOWNLOAD_SKIP_VER || node-gyp rebuild\",\n+    \"install\": \"echo 'Skipping re2 installation.'\",\n     \"verify-build\": \"node scripts/verify-build.js\",\n     \"rebuild\": \"node-gyp rebuild\"\n   },\ndiff --git a/re2.js b/re2.js\nindex 3f32be925d96464242e503c03855c37ac5b354af..fdecf3f1a46d9f36a81b211fd5dbba9426b4bceb 100644\n--- a/re2.js\n+++ b/re2.js\n@@ -1,38 +1,3 @@\n 'use strict';\n \n-const RE2 = require('./build/Release/re2.node');\n-\n-if (typeof Symbol != 'undefined') {\n-  Symbol.match &&\n-    (RE2.prototype[Symbol.match] = function (str) {\n-      return this.match(str);\n-    });\n-  Symbol.search &&\n-    (RE2.prototype[Symbol.search] = function (str) {\n-      return this.search(str);\n-    });\n-  Symbol.replace &&\n-    (RE2.prototype[Symbol.replace] = function (str, repl) {\n-      return this.replace(str, repl);\n-    });\n-  Symbol.split &&\n-    (RE2.prototype[Symbol.split] = function (str, limit) {\n-      return this.split(str, limit);\n-    });\n-  Symbol.matchAll &&\n-    (RE2.prototype[Symbol.matchAll] = function* (str) {\n-      if (!this.global) {\n-        throw TypeError('String.prototype.matchAll called with a non-global RE2 argument');\n-      }\n-      const re = new RE2(this);\n-      re.lastIndex = this.lastIndex;\n-      for (;;) {\n-        const result = re.exec(str);\n-        if (!result) break;\n-        if (result[0] === '') ++re.lastIndex;\n-        yield result;\n-      }\n-    });\n-}\n-\n-module.exports = RE2;\n+module.exports = RegExp;\n"
  },
  {
    "path": "postcss.config.js",
    "content": "export default {\n\tplugins: {\n\t\ttailwindcss: {},\n\t\tautoprefixer: {},\n\t},\n}\n"
  },
  {
    "path": "scripts/resolve-ens.js",
    "content": "import fs from 'fs'\nimport { normalize } from 'viem/ens'\nimport { mainnet } from 'viem/chains'\nimport { createPublicClient, http } from 'viem'\n\nexport default async () => {\n\tif (!process.env.NEXT_PUBLIC_AUTHOR_ENS) {\n\t\tconsole.error('NEXT_PUBLIC_AUTHOR_ENS is not set')\n\t}\n\n\tif (fs.existsSync('./src/data/ens.ts')) {\n\t\t// If the author ENS wasn't set on the first run, we might have an invalid publication address.\n\t\t// This makes sure invalid addresses are re-calculated on every run instead of just the first one.\n\t\tif (!String(fs.readFileSync('./src/data/ens.ts')).includes(\"'null'\")) return\n\t}\n\n\tconst client = createPublicClient({ chain: mainnet, transport: http(process.env.NEXT_PUBLIC_RPC_URL) })\n\n\tconst publicationAddress = await client.getEnsAddress({ name: normalize(process.env.NEXT_PUBLIC_AUTHOR_ENS) })\n\n\tfs.writeFileSync('./src/data/ens.ts', `export const contributorAddresses = ['${publicationAddress}']\\n`)\n}\n"
  },
  {
    "path": "src/app/[digest]/page.tsx",
    "content": "import { unified } from 'unified'\nimport rehypeRaw from 'rehype-raw'\nimport strip from 'strip-markdown'\nimport remarkParse from 'remark-parse'\nimport { getEntry } from '@/data/entries'\nimport { notFound } from 'next/navigation'\nimport { uriTransformer } from '@/utils/url'\nimport { format as timeago } from 'timeago.js'\nimport remarkStringify from 'remark-stringify'\nimport { formatAddress } from '@/utils/address'\nimport getPublication from '@/data/publication'\nimport { SetImageSizes } from '@/context/image_sizes'\nimport highlightCode from '@/utils/highlightMarkdown'\nimport type { Metadata, ResolvingMetadata } from 'next'\nimport ReactMarkdown, { Components } from 'react-markdown'\nimport { Image, LinkOrEmbed, BlockQuote, Block } from '@/utils/markdown'\n\nexport const revalidate = 1 * 60 * 60 // refresh article contents every hour\n\nconst components: Components = {\n\timg: Image,\n\ta: LinkOrEmbed,\n\tblockquote: BlockQuote,\n\tp: Block,\n}\n\ntype Props = {\n\tparams: { digest: string }\n}\n\nexport async function generateMetadata({ params: { digest } }: Props, parent: ResolvingMetadata): Promise<Metadata> {\n\tconst [publication, entry] = await Promise.all([getPublication(), getEntry(digest)])\n\tif (!entry) return notFound()\n\n\t// If there's an image we want to use the second paragraph as the description instead of the first one.\n\t// We'll also strip the markdown from the description (to avoid things like links showing up) and trim the newline at the end\n\tconst description = String(\n\t\tawait unified()\n\t\t\t.use(remarkParse)\n\t\t\t.use(strip)\n\t\t\t.use(remarkStringify)\n\t\t\t.process(entry.body.split('\\n\\n')[entry.cover_image ? 1 : 0])\n\t).slice(0, -1)\n\n\treturn {\n\t\tdescription,\n\t\ttitle: `${entry.title} — Mirror`,\n\t\topenGraph: {\n\t\t\tdescription,\n\t\t\ttitle: `${entry.title} — Mirror`,\n\t\t\timages: entry.cover_image ? [{ url: entry.cover_image }] : [],\n\t\t},\n\t\ttwitter: {\n\t\t\tdescription,\n\t\t\tcard: 'summary_large_image',\n\t\t\ttitle: `${entry.title} — Mirror`,\n\t\t\timages: entry.cover_image ? [{ url: entry.cover_image }] : [],\n\t\t},\n\t}\n}\n\nconst Article = async ({ params: { digest } }: Props) => {\n\tconst [publication, entry] = await Promise.all([getPublication(), getEntry(digest)])\n\tif (!entry) return notFound()\n\n\tconst body = String(\n\t\tawait unified()\n\t\t\t.use(remarkParse) // Parse markdown\n\t\t\t.use(highlightCode, { theme: publication.theme.colorMode == 'DARK' ? 'dark' : 'light' }) // Highlight code\n\t\t\t.use(remarkStringify) // Serialize markdown\n\t\t\t.process(entry.body)\n\t)\n\n\treturn (\n\t\t<article>\n\t\t\t<header>\n\t\t\t\t<h1 className=\"text-gray-900 dark:text-gray-200 text-3xl sm:text-5xl font-bold\">{entry.title}</h1>\n\t\t\t\t<div className=\"flex flex-wrap items-center my-4 gap-x-4 gap-y-2 max-w-xl\">\n\t\t\t\t\t{publication.members.map(contributor => (\n\t\t\t\t\t\t<div className=\"flex items-center\" key={contributor.address}>\n\t\t\t\t\t\t\t<img className=\"rounded-full w-10 h-10\" src={contributor.avatarURL} />\n\t\t\t\t\t\t\t<div className=\"flex items-center tracking-normal text-gray-800 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t<div className=\"flex items-center p-1 leading-5\">\n\t\t\t\t\t\t\t\t\t<p className=\"mr-2 font-medium\">{contributor.displayName}</p>\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\tclassName=\"bg-gray-100 dark:bg-gray-800  rounded-full px-1.5 font-medium text-sm text-gray-400  dark:text-gray-500\"\n\t\t\t\t\t\t\t\t\t\thref={`https://etherscan.io/address/${contributor.address}`}\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{formatAddress(contributor.address)}\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t))}\n\t\t\t\t\t<div className=\"flex items-center\">\n\t\t\t\t\t\t<time\n\t\t\t\t\t\t\tdateTime={new Date(entry.timestamp * 1000).toISOString()}\n\t\t\t\t\t\t\tclassName=\"block bg-gray-100 dark:bg-gray-800 rounded-full px-2 py-0.5 font-medium text-sm text-gray-400 dark:text-gray-500\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{timeago(entry.timestamp * 1000)}\n\t\t\t\t\t\t</time>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</header>\n\n\t\t\t<div className=\"prose lg:prose-lg dark:prose-dark pb-10\">\n\t\t\t\t<SetImageSizes sizes={entry.image_sizes}>\n\t\t\t\t\t<ReactMarkdown components={components} urlTransform={uriTransformer} rehypePlugins={[rehypeRaw]}>\n\t\t\t\t\t\t{body}\n\t\t\t\t\t</ReactMarkdown>\n\t\t\t\t</SetImageSizes>\n\t\t\t</div>\n\n\t\t\t{publication.mailingListURL && (\n\t\t\t\t<div className=\"flex items-center justify-center mb-10\">\n\t\t\t\t\t<a\n\t\t\t\t\t\thref={publication.mailingListURL}\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t\tclassName=\"bg-blue-100 dark:bg-yellow-400 dark:bg-opacity-20 font-medium text-blue-500 dark:text-yellow-300 rounded-lg p-4 hover:ring-4 ring-blue-100 dark:ring-yellow-400 dark:ring-opacity-20 transition duration-300 text-base shadow-xs hover:shadow-xs sm:text-lg sm:px-10 inline-flex items-center space-x-2\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\tclassName=\"w-6 h-6\"\n\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\t\td=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t<span>Subscribe</span>\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t<footer className=\"border dark:border-gray-800 rounded-lg divide-y dark:divide-gray-800 font-mono max-w-xl mx-auto\">\n\t\t\t\t{entry.transaction && (\n\t\t\t\t\t<a\n\t\t\t\t\t\thref={`https://viewblock.io/arweave/tx/${entry.transaction}`}\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t\tclassName=\"flex items-center justify-between text-gray-400 dark:text-gray-500 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"flex items-center\">\n\t\t\t\t\t\t\t<p className=\"uppercase text-xs mr-1\">Arweave TX</p>\n\t\t\t\t\t\t\t<p className=\"transform text-xs -rotate-45 font-sans\">&rarr;</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<p className=\"text-xs\">{formatAddress(entry.transaction)}</p>\n\t\t\t\t\t</a>\n\t\t\t\t)}\n\t\t\t\t<a\n\t\t\t\t\thref={`https://etherscan.io/address/${entry.contributor}`}\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\tclassName=\"flex items-center justify-between text-gray-400 dark:text-gray-500 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition\"\n\t\t\t\t>\n\t\t\t\t\t<div className=\"flex items-center\">\n\t\t\t\t\t\t<p className=\"uppercase text-xs mr-1\">Ethereum Address</p>\n\t\t\t\t\t\t<p className=\"transform text-xs -rotate-45 font-sans\">&rarr;</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p className=\"text-xs\">{formatAddress(entry.contributor)}</p>\n\t\t\t\t</a>\n\t\t\t\t<div className=\"flex items-center justify-between text-gray-400 dark:text-gray-500 px-4 py-3\">\n\t\t\t\t\t<p className=\"uppercase text-xs\">Content Digest</p>\n\t\t\t\t\t<p className=\"text-xs\">{formatAddress(entry.digest)}</p>\n\t\t\t\t</div>\n\t\t\t</footer>\n\t\t</article>\n\t)\n}\n\nexport default Article\n"
  },
  {
    "path": "src/app/api/link-preview/route.ts",
    "content": "import metascraper from 'metascraper'\nimport urlFetcher from 'metascraper-url'\nimport langFetcher from 'metascraper-lang'\nimport dateFetcher from 'metascraper-date'\nimport logoFetcher from 'metascraper-logo'\nimport titleFetcher from 'metascraper-title'\nimport imageFetcher from 'metascraper-image'\nimport authorFetcher from 'metascraper-author'\nimport { Metadata } from '@/types/link-preview'\nimport clearbitFetcher from 'metascraper-clearbit'\nimport { calculateImageSize } from '@/utils/images'\nimport publisherFetcher from 'metascraper-publisher'\nimport faviconFetcher from 'metascraper-logo-favicon'\nimport { NextRequest, NextResponse } from 'next/server'\nimport descriptionFetcher from 'metascraper-description'\n\nconst scraper = metascraper([\n\ttitleFetcher(),\n\tdescriptionFetcher(),\n\tlangFetcher(),\n\tauthorFetcher(),\n\tpublisherFetcher(),\n\timageFetcher(),\n\tdateFetcher(),\n\turlFetcher(),\n\tlogoFetcher(),\n\tclearbitFetcher(),\n\tfaviconFetcher(),\n])\n\nexport async function GET(request: NextRequest): Promise<NextResponse> {\n\tconst url = request.nextUrl.searchParams.get('url')\n\n\tif (!url) return NextResponse.json({ error: 'Missing url parameter' }, { status: 422 })\n\n\treturn NextResponse.json(await getMeta(url))\n}\n\nconst getMeta = async (url: string): Promise<Metadata> => {\n\tconst html = await fetch(url).then(res => res.text())\n\tconst meta = await scraper({ html, url })\n\n\tawait Promise.all(\n\t\t['image', 'logo'].map(async key => {\n\t\t\tif (!meta[key]) return\n\n\t\t\t// @ts-ignore\n\t\t\tmeta[key] = {\n\t\t\t\turl: meta[key],\n\t\t\t\t...(await calculateImageSize(meta[key]!)),\n\t\t\t}\n\t\t})\n\t)\n\n\treturn meta as Metadata\n}\n"
  },
  {
    "path": "src/app/api/nft-data/route.ts",
    "content": "import ERC721 from '@/data/ERC721'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { mainnet, base, optimism, zora, polygon } from 'viem/chains'\nimport { createPublicClient, http, getContract, extractChain } from 'viem'\n\nexport async function GET(request: NextRequest): Promise<NextResponse> {\n\tconst chainId = request.nextUrl.searchParams.get('chain')\n\tconst tokenId = request.nextUrl.searchParams.get('tokenId') as bigint | null\n\tconst contractAddress = request.nextUrl.searchParams.get('contract') as `0x${string}` | null\n\n\tif (!chainId || !contractAddress || !tokenId) {\n\t\treturn NextResponse.json({ error: 'Missing chain, contract or tokendId parameters' }, { status: 400 })\n\t}\n\n\tconst chain = extractChain({\n\t\t// @ts-ignore\n\t\tid: parseInt(chainId),\n\t\tchains: [mainnet, base, optimism, zora, polygon],\n\t})\n\n\tconst contract = getContract({\n\t\tabi: ERC721,\n\t\taddress: contractAddress,\n\t\tclient: createPublicClient({ chain, transport: http() }),\n\t})\n\n\tconst token = await fetch(buildURL(await contract.read.tokenURI([tokenId]))).then(res => res.json())\n\n\treturn NextResponse.json({\n\t\t...token,\n\t\ttokenId,\n\t\tcontractAddress,\n\t\timage: buildURL(token.image),\n\t\tmimeType: await getMimeType(buildURL(token.image)),\n\t})\n}\n\nconst buildURL = (rawURL: string): string => {\n\tconst url = new URL(rawURL)\n\n\tswitch (url.protocol) {\n\t\tcase 'ipfs:':\n\t\t\treturn `https://ipfs.io/ipfs/${url.hostname}${url.pathname}`\n\t\tcase 'ar:':\n\t\tcase 'arweave:':\n\t\t\treturn `https://arweave.net/${url.hostname}${url.pathname}`\n\n\t\tdefault:\n\t\t\treturn rawURL\n\t}\n}\n\nconst getMimeType = async (url: string): Promise<string | null> => {\n\treturn fetch(url, { method: 'HEAD' }).then(res => res.headers.get('Content-Type'))\n}\n"
  },
  {
    "path": "src/app/error.tsx",
    "content": "'use client'\n\nimport Link from 'next/link'\nimport { FC, useEffect } from 'react'\n\nconst InternalError: FC = ({ error }: { error: Error & { digest?: string } }) => {\n\tuseEffect(() => {\n\t\tconsole.error(error)\n\t}, [error])\n\n\treturn (\n\t\t<div className=\"absolute inset-0 flex flex-col space-y-10 items-center justify-center h-full\">\n\t\t\t<p className=\"text-gray-800 text-3xl dark:text-gray-300 font-semibold\">Internal Error</p>\n\t\t\t<Link\n\t\t\t\thref=\"/\"\n\t\t\t\tclassName=\"bg-blue-100 dark:bg-fuchsia-400 dark:bg-opacity-20 font-medium text-blue-500 dark:text-fuchsia-300 rounded-lg px-4 py-3 hover:ring-4 ring-blue-50 dark:ring-fuchsia-400 dark:ring-opacity-20 transition duration-300 text-base shadow-xs hover:shadow-xs sm:text-lg\"\n\t\t\t>\n\t\t\t\tReturn Home\n\t\t\t</Link>\n\t\t</div>\n\t)\n}\n\nexport default InternalError\n"
  },
  {
    "path": "src/app/feed.xml/route.ts",
    "content": "import RSS from 'rss'\nimport { unified } from 'unified'\nimport remarkParse from 'remark-parse'\nimport getEntries from '@/data/entries'\nimport remark2rehype from 'remark-rehype'\nimport rehypeStringify from 'rehype-stringify'\nimport getPublication from '@/data/publication'\nimport { NextRequest, NextResponse } from 'next/server'\nimport highlightMarkdown from '@/utils/highlightMarkdown'\n\nexport async function GET(request: NextRequest): Promise<NextResponse> {\n\tconst host = request.headers.get('host')\n\n\tlet [publication, entries] = await Promise.all([getPublication(), getEntries()])\n\n\tconst markdownRenderer = unified()\n\t\t.use(remarkParse) // Parse markdown\n\t\t.use(highlightMarkdown, { theme: 'light' }) // Highlight code, doesn't work on production yet\n\t\t.use(remark2rehype, { allowDangerousHtml: true }) // Convert to HTML\n\t\t.use(rehypeStringify) // Serialize HTML\n\n\tconst feed = new RSS({\n\t\ttitle: publication.displayName,\n\t\tdescription: publication.description || 'A Mirror publication',\n\t\timage_url: publication?.headerImage?.url,\n\t\tmanagingEditor: publication.members.length == 1 ? publication.members[0].displayName : publication.displayName,\n\t\twebMaster: 'MirrorXYZ',\n\t\tttl: 1 * 60, // cache for an hour\n\t\tsite_url: `https://${host}/`,\n\t\tgenerator: 'RSS for Mirror, by Miguel Piedrafita',\n\t\tfeed_url: `https://${host}/feed.xml`,\n\t})\n\n\tawait Promise.all(\n\t\tentries.map(async entry => {\n\t\t\tconst body = String(await markdownRenderer.process(entry.body))\n\n\t\t\tfeed.item({\n\t\t\t\ttitle: entry.title,\n\t\t\t\tguid: entry.digest,\n\t\t\t\turl: `https://${host}/${entry.digest}`,\n\t\t\t\tdate: new Date(entry.timestamp * 1000).toISOString(),\n\t\t\t\tdescription: body,\n\t\t\t})\n\t\t})\n\t)\n\n\treturn new NextResponse(feed.xml({ indent: true }), {\n\t\theaders: {\n\t\t\t'Content-Type': 'application/rss+xml',\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import '@/styles/style.css'\nimport Link from 'next/link'\nimport { Metadata } from 'next'\nimport { Inter } from 'next/font/google'\nimport { SetTheme } from '@/context/theme'\nimport getPublication from '@/data/publication'\n\nconst inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap' })\n\nexport async function generateMetadata(): Promise<Metadata> {\n\tconst publication = await getPublication()\n\n\treturn {\n\t\ttitle: `${publication.displayName} — Mirror`,\n\t\tdescription: publication.description,\n\t\topenGraph: {\n\t\t\ttitle: `${publication.displayName} — Mirror`,\n\t\t\tdescription: publication.description,\n\t\t\timages: [{ url: publication.avatarURL }],\n\t\t},\n\t\ttwitter: {\n\t\t\tcard: 'summary',\n\t\t\tdescription: publication.description,\n\t\t\timages: [{ url: publication.avatarURL }],\n\t\t\ttitle: `${publication.displayName} — Mirror`,\n\t\t},\n\t\talternates: {\n\t\t\ttypes: {\n\t\t\t\t'application/rss+xml': '/feed.xml',\n\t\t\t},\n\t\t},\n\t}\n}\n\nconst RootLayout = async ({ children }: { children: React.ReactNode }) => {\n\tconst publication = await getPublication()\n\n\treturn (\n\t\t<html lang=\"en\" className={inter.variable}>\n\t\t\t<body className={publication.theme.colorMode == 'DARK' ? 'dark' : ''}>\n\t\t\t\t<div className=\"font-sans dark:bg-gray-900 min-h-screen\">\n\t\t\t\t\t<header className=\"p-4\">\n\t\t\t\t\t\t<Link href=\"/\" className=\"flex items-center space-x-4\">\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={publication.avatarURL}\n\t\t\t\t\t\t\t\tclassName=\"ring-1 ring-gray-200 dark:ring-gray-700 rounded-full w-14 h-14 hover:ring-4 transition ease-in-out\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<h1 className=\"text-xl font-medium dark:text-gray-200\">{publication.displayName}</h1>\n\t\t\t\t\t\t\t\t<div className=\"flex items-center space-x-2\">\n\t\t\t\t\t\t\t\t\t<span className=\"rounded-full bg-gray-200 dark:bg-gray-800 px-1 text-sm text-gray-500 font-medium\">\n\t\t\t\t\t\t\t\t\t\tENS\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<p className=\"text-gray-400 dark:text-gray-600 pb-0.5\">{publication.ens}</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Link>\n\t\t\t\t\t</header>\n\t\t\t\t\t<main className=\"max-w-3xl mx-auto py-16 px-4 sm:px-0\">\n\t\t\t\t\t\t<SetTheme theme={publication.theme}>{children}</SetTheme>\n\t\t\t\t\t</main>\n\t\t\t\t</div>\n\t\t\t</body>\n\t\t</html>\n\t)\n}\n\nexport default RootLayout\n"
  },
  {
    "path": "src/app/not-found.tsx",
    "content": "import { FC } from 'react'\nimport getPublication from '@/data/publication'\nimport LinkButton from '@/components/LinkButton'\n\nconst PageNotFound: FC = async () => {\n\tconst publication = await getPublication()\n\n\treturn (\n\t\t<div className=\"absolute inset-0 flex flex-col space-y-10 items-center justify-center h-full\">\n\t\t\t<p className=\"text-gray-800 text-3xl dark:text-gray-300 font-semibold\">Page Not Found</p>\n\t\t\t<LinkButton href=\"/\" accentColor={publication.theme.accent}>\n\t\t\t\tReturn Home\n\t\t\t</LinkButton>\n\t\t</div>\n\t)\n}\n\nexport default PageNotFound\n"
  },
  {
    "path": "src/app/page.tsx",
    "content": "import Link from 'next/link'\nimport getEntries from '@/data/entries'\nimport { getExcerpt } from '@/utils/excerpt'\nimport { format as timeago } from 'timeago.js'\nimport getPublication from '@/data/publication'\nimport LinkButton from '@/components/LinkButton'\nimport { SetImageSizes } from '@/context/image_sizes'\nimport ReactMarkdown, { Components } from 'react-markdown'\nimport { BlockQuote, LinkOrEmbed, Block, Image } from '@/utils/markdown'\n\nexport const revalidate = 5 * 60 // refresh page index every 5 minutes\n\nconst components: Components = {\n\timg: Image,\n\ta: LinkOrEmbed,\n\tblockquote: BlockQuote,\n\tp: Block,\n}\n\nconst Index = async () => {\n\tconst [publication, entries] = await Promise.all([getPublication(), getEntries()])\n\n\treturn (\n\t\t<div className=\"space-y-32 mb-10\">\n\t\t\t{entries.map(entry => (\n\t\t\t\t<article key={entry.digest}>\n\t\t\t\t\t<Link\n\t\t\t\t\t\thref={`/${entry.transaction}`}\n\t\t\t\t\t\tclassName=\"text-gray-900 dark:text-gray-200 text-3xl sm:text-5xl font-bold\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{entry.title}\n\t\t\t\t\t</Link>\n\t\t\t\t\t<div className=\"flex flex-wrap items-center my-4 gap-x-4 gap-y-2 max-w-xl\">\n\t\t\t\t\t\t{publication.members.map(contributor => (\n\t\t\t\t\t\t\t<div className=\"flex items-center\" key={contributor.address}>\n\t\t\t\t\t\t\t\t<img className=\"rounded-full w-10 h-10\" src={contributor.avatarURL} />\n\t\t\t\t\t\t\t\t<div className=\"flex items-center tracking-normal text-gray-800 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center p-1 leading-5\">\n\t\t\t\t\t\t\t\t\t\t<p className=\"mr-2 font-medium\">{contributor.displayName}</p>\n\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"bg-gray-100 dark:bg-gray-800 rounded-full px-1.5 font-medium text-sm text-gray-400  dark:text-gray-500\"\n\t\t\t\t\t\t\t\t\t\t\thref={`https://etherscan.io/address/${contributor.address}`}\n\t\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{contributor.address.substr(0, 6)}\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t<div className=\"flex items-center\">\n\t\t\t\t\t\t\t<time\n\t\t\t\t\t\t\t\tdateTime={new Date(entry.timestamp * 1000).toISOString()}\n\t\t\t\t\t\t\t\tclassName=\"block bg-gray-100 dark:bg-gray-800 rounded-full px-2 py-0.5 font-medium text-sm text-gray-400 dark:text-gray-500\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{timeago(entry.timestamp * 1000)}\n\t\t\t\t\t\t\t</time>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"prose lg:prose-lg dark:prose-dark mb-8\">\n\t\t\t\t\t\t<SetImageSizes sizes={entry.image_sizes}>\n\t\t\t\t\t\t\t<ReactMarkdown components={components}>{getExcerpt(entry.body)}</ReactMarkdown>\n\t\t\t\t\t\t</SetImageSizes>\n\t\t\t\t\t</div>\n\t\t\t\t\t{entry.body.split('\\n\\n').length > 4 && (\n\t\t\t\t\t\t<LinkButton href={`/${entry.transaction}`} accentColor={publication.theme.accent}>\n\t\t\t\t\t\t\tContinue Reading\n\t\t\t\t\t\t</LinkButton>\n\t\t\t\t\t)}\n\t\t\t\t</article>\n\t\t\t))}\n\t\t\t{entries.length === 0 && (\n\t\t\t\t<div className=\"absolute inset-0 flex items-center justify-center h-full\">\n\t\t\t\t\t<p className=\"text-gray-400 dark:text-gray-600 font-medium\">Patience</p>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nexport default Index\n"
  },
  {
    "path": "src/app/post/[slug]/route.ts",
    "content": "import getEntries from '@/data/entries'\nimport { notFound, redirect } from 'next/navigation'\nimport { NextRequest, NextResponse } from 'next/server'\n\nexport async function GET(\n\trequest: NextRequest,\n\t{ params: { slug } }: { params: { slug: string } }\n): Promise<NextResponse> {\n\tconst entry = (await getEntries()).filter(entry => entry.slug === slug)?.[0]\n\n\tif (!entry) return notFound()\n\treturn redirect(`/${entry.digest}`)\n}\n"
  },
  {
    "path": "src/app/posts.json/route.ts",
    "content": "import getEntries from '@/data/entries'\nimport { NextRequest, NextResponse } from 'next/server'\n\nexport async function GET(request: NextRequest): Promise<NextResponse> {\n\tconst host = request.headers.get('host')\n\tconst entries = (await getEntries()).map(entry => ({ ...entry, url: `https://${host}/post/${entry.slug}` }))\n\n\treturn NextResponse.json(entries)\n}\n"
  },
  {
    "path": "src/components/EntryLink.tsx",
    "content": "'use client'\n\nimport Link from 'next/link'\nimport { useTheme } from '@/context/theme'\nimport { FC, PropsWithChildren } from 'react'\n\ntype Props = PropsWithChildren<{ href: string; className?: string }>\n\nconst EntryLink: FC<Props> = ({ href, children, className }) => {\n\tconst { accent } = useTheme()\n\tclassName = className ? className : getClass(accent)\n\n\tif (href.startsWith('/') || (typeof window !== 'undefined' && href.startsWith(window.location.origin))) {\n\t\treturn (\n\t\t\t<Link href={href} className={className}>\n\t\t\t\t{children}\n\t\t\t</Link>\n\t\t)\n\t}\n\n\treturn (\n\t\t// eslint-disable-next-line react/jsx-no-target-blank\n\t\t<a href={href} target={href.startsWith('#') ? '' : '_blank'} rel=\"noopener\" className={className}>\n\t\t\t{children}\n\t\t</a>\n\t)\n}\n\nconst getClass = (accentColor: string): string => {\n\tswitch (accentColor.toLowerCase()) {\n\t\tcase 'purple':\n\t\t\treturn '!text-fuchsia-400'\n\t\tcase 'pink':\n\t\t\treturn '!text-red-500'\n\t\tcase 'red':\n\t\t\treturn '!text-red-500'\n\t\tcase 'orange':\n\t\t\treturn '!text-orange-400'\n\t\tcase 'yellow':\n\t\t\treturn '!text-yellow-400'\n\t\tcase 'teal':\n\t\t\treturn '!text-cyan-400'\n\t\tcase 'blue':\n\t\t\treturn '!text-blue-500'\n\t\tcase 'indigo':\n\t\t\treturn '!text-indigo-400'\n\t\tcase 'green':\n\t\t\treturn '!text-emerald-400'\n\t\tcase 'foreground':\n\t\t\treturn '!text-white'\n\n\t\tdefault:\n\t\t\treturn '!text-blue-400'\n\t}\n}\n\nexport default EntryLink\n"
  },
  {
    "path": "src/components/LinkButton.tsx",
    "content": "import Link, { LinkProps } from 'next/link'\nimport { FC, PropsWithChildren } from 'react'\n\ntype Props = {\n\taccentColor: string\n} & LinkProps\n\nconst LinkButton: FC<PropsWithChildren<Props>> = ({ href, children, accentColor, ...props }) => {\n\treturn (\n\t\t<Link\n\t\t\t{...props}\n\t\t\thref={href}\n\t\t\tclassName={`${getClasses(\n\t\t\t\taccentColor\n\t\t\t)} dark:bg-opacity-20 font-medium rounded-lg p-4 hover:ring-4 dark:ring-opacity-20 transition duration-300 text-base shadow-xs hover:shadow-xs sm:text-lg sm:px-10`}\n\t\t>\n\t\t\t{children}\n\t\t</Link>\n\t)\n}\n\nconst getClasses = (accentColor: string): string => {\n\tswitch (accentColor.toLowerCase()) {\n\t\tcase 'yellow':\n\t\t\treturn 'bg-yellow-100 dark:bg-yellow-400 text-yellow-500 dark:text-yellow-300 ring-yellow-100 dark:ring-yellow-400'\n\t\tcase 'purple':\n\t\t\treturn 'bg-fuchsia-100 dark:bg-fuchsia-400 text-fuchsia-500 dark:text-fuchsia-300 ring-fuchsia-100 dark:ring-fuchsia-400'\n\t\tcase 'pink':\n\t\tcase 'red':\n\t\t\treturn 'bg-red-100 dark:bg-red-400 text-red-500 dark:text-red-300 ring-red-100 dark:ring-red-400'\n\t\tcase 'orange':\n\t\t\treturn 'bg-orange-100 dark:bg-orange-400 text-orange-500 dark:text-orange-300 ring-orange-100 dark:ring-orange-400'\n\t\tcase 'teal':\n\t\t\treturn 'bg-cyan-100 dark:bg-cyan-400 text-cyan-500 dark:text-cyan-300 ring-cyan-100 dark:ring-cyan-400'\n\t\tcase 'indigo':\n\t\t\treturn 'bg-indigo-100 dark:bg-indigo-400 text-indigo-500 dark:text-indigo-300 ring-indigo-100 dark:ring-indigo-400'\n\t\tcase 'green':\n\t\t\treturn 'bg-emerald-100 dark:bg-emerald-400 text-emerald-500 dark:text-emerald-300 ring-emerald-100 dark:ring-emerald-400'\n\t\tcase 'foreground':\n\t\t\treturn 'bg-white text-white ring-white'\n\n\t\tcase 'blue':\n\t\tdefault:\n\t\t\treturn 'bg-blue-100 dark:bg-blue-400 text-blue-500 dark:text-blue-300 ring-blue-100 dark:ring-blue-400'\n\t}\n}\n\nexport default LinkButton\n"
  },
  {
    "path": "src/components/NFT.tsx",
    "content": "'use client'\n\nimport useSWR from 'swr'\nimport { useEffect, useRef, useState } from 'react'\n\nconst NFT = ({ data: { name, image, contractAddress, tokenId, mimeType } }) => {\n\treturn (\n\t\t<div\n\t\t\tclassName=\"shadow dark:shadow-none dark:bg-black rounded-lg max-w-lg mx-auto not-prose overflow-hidden group\"\n\t\t\tdata-nft\n\t\t>\n\t\t\t<section className=\"flex items-center justify-between py-5 px-6\">\n\t\t\t\t<p className=\"font-semibold text-gray-700 dark:text-gray-300 text-lg\">{name}</p>\n\t\t\t\t<a\n\t\t\t\t\tclassName=\"bg-gray-100 dark:bg-gray-900 !rounded-full py-2 px-2.5 block\"\n\t\t\t\t\thref={`https://etherscan.io/nft/${contractAddress}/${tokenId}`}\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t>\n\t\t\t\t\t<svg viewBox=\"0 0 177 68\" fill=\"currentColor\" className=\"w-6 text-gray-500 dark:text-gray-600\">\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\td=\"M17.405 64.127l5.324-24.978c1.127-5.098 2.15-11.419 2.56-15.802.819 3.058 2.662 8.767 4.3 13.05l10.75 28.24c1.024 2.548 1.331 2.956 2.355 3.262.41.081.492.098 1.556.101h11.606c3.232-.02 3.365-.3 4.141-3.873L72.693 4.894c.205-1.02.307-1.937.307-2.345 0-.816-.307-1.326-.921-1.937C71.464.102 71.157 0 68.597 0h-8.19c-3.89 0-3.993.102-4.812 3.874L50.68 26.813c-.921 4.588-2.355 13.05-2.764 17.637-1.024-3.568-2.867-9.073-4.403-13.05L33.07 3.365C32.046.816 31.842.408 30.715.102a16.23 16.23 0 00-.14-.028l-.126-.022c-.246-.039-.491-.049-1.29-.051L28.448 0H17.712c-3.788 0-3.89.102-4.71 3.874L.308 63.107C.102 64.127 0 65.044 0 65.452c0 .815.307 1.325.921 1.937.588.487.895.602 3.16.611h9.096c3.32-.02 3.452-.3 4.228-3.873zm65.413 0l4.812-22.328h25.35c3.233-.02 3.366-.3 4.142-3.874l1.126-5.097c.41-1.733.41-1.835.41-2.345 0-.816-.308-1.325-.922-1.937-.614-.51-.922-.612-3.482-.612H90.6l2.765-13.151h29.184c3.789 0 3.891-.102 4.71-3.772l1.331-6.117c.362-1.53.404-1.789.41-2.179v-.166c0-.816-.307-1.326-.922-1.937-.614-.51-.921-.612-3.481-.612H82.715c-3.789 0-3.891.102-4.71 3.772L65.307 63.107c-.205 1.02-.307 1.937-.307 2.345 0 .815.307 1.325.922 1.937.587.487.894.602 3.16.611h9.595c3.334-.02 3.46-.295 4.14-3.873zm61.259 0l10.564-49.344h16c3.795 0 3.897-.102 4.718-3.772l1.333-6.117c.205-1.122.308-1.836.308-2.345 0-.816-.308-1.326-.923-1.937-.615-.51-.923-.612-3.487-.612h-49.231c-3.795 0-3.897.102-4.718 3.772l-1.333 6.117c-.205 1.02-.308 1.937-.308 2.345 0 .816.308 1.325.923 1.937.615.51.923.612 3.487.612h15.385l-10.257 48.324c-.307 1.427-.41 1.835-.41 2.345 0 .815.308 1.325.923 1.937.589.487.896.602 3.166.611h9.624c3.326-.02 3.459-.3 4.236-3.873z\"\n\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\tfillRule=\"nonzero\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</svg>\n\t\t\t\t</a>\n\t\t\t</section>\n\n\t\t\t{image && mimeType && (\n\t\t\t\t<section className=\"nfte__media\">\n\t\t\t\t\t<Media media={image} mediaMimeType={mimeType} />\n\t\t\t\t</section>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nconst Loading = () => (\n\t<div className=\"shadow dark:shadow-none dark:bg-black rounded-lg max-w-lg mx-auto\">\n\t\t<div className=\"py-16 px-8 flex items-center justify-center\">\n\t\t\t<svg\n\t\t\t\tclassName=\"w-6 h-6 animate-spin text-black dark:text-white text-opacity-40\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t>\n\t\t\t\t<circle fill=\"none\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeDasharray=\"32\" cx=\"12\" cy=\"12\" r=\"10\" />\n\t\t\t\t<circle fill=\"none\" strokeWidth=\"2\" strokeLinecap=\"round\" cx=\"12\" cy=\"12\" r=\"10\" opacity=\"0.25\" />\n\t\t\t</svg>\n\t\t</div>\n\t</div>\n)\n\nconst TextMedia = ({ media }) => {\n\tconst [content, setContent] = useState<string | null>(null)\n\n\tuseEffect(() => {\n\t\tfetch(media)\n\t\t\t.then(r => r.text())\n\t\t\t.then(r => setContent(r))\n\t}, [])\n\n\treturn <div>{content}</div>\n}\n\nconst VideoMedia = ({ media }) => {\n\tconst videoRef = useRef<HTMLVideoElement>(null)\n\tconst [isMuted, setMuted] = useState(true)\n\n\tuseEffect(() => {\n\t\tif (!videoRef.current) return\n\n\t\tvideoRef.current.muted = isMuted\n\t}, [isMuted])\n\n\treturn (\n\t\t<div className=\"relative\">\n\t\t\t<video ref={videoRef} muted autoPlay={true} controls={false} loop playsInline>\n\t\t\t\t<source src={media} />\n\t\t\t</video>\n\t\t\t<button\n\t\t\t\tonClick={() => setMuted(state => !state)}\n\t\t\t\tclassName=\"absolute bottom-2 right-2 bg-white bg-opacity-10 !rounded-full p-2 text-white/30 transition group-hover:text-white/80 focus:outline-none\"\n\t\t\t\tstyle={{ backdropFilter: 'blur(2px) brightness(1.3)' }}\n\t\t\t>\n\t\t\t\t<svg\n\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tclassName=\"transition duration-200 w-6 h-6\"\n\t\t\t\t>\n\t\t\t\t\t{isMuted ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\t\td=\"M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z\"\n\t\t\t\t\t\t\t\tclipRule=\"evenodd\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\t\td=\"M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\td=\"M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</svg>\n\t\t\t</button>\n\t\t</div>\n\t)\n}\n\nconst AudioMedia = ({ media }) => <audio controls src={media} />\n\nconst Media = ({ media, mediaMimeType }) => {\n\tif (mediaMimeType?.includes('text')) return <TextMedia media={media} />\n\tif (mediaMimeType?.includes('video')) return <VideoMedia media={media} />\n\tif (mediaMimeType?.includes('audio')) return <AudioMedia media={media} />\n\n\treturn <img src={media} />\n}\n\nconst Embed = ({ chainId, contract, tokenId }) => {\n\tconst { data, error, isLoading } = useSWR(\n\t\t() => `/api/nft-data?chain=${chainId}&contract=${contract}&tokenId=${tokenId}`,\n\t\turl => fetch(url).then(r => r.json())\n\t)\n\n\tif (error) {\n\t\treturn (\n\t\t\t<div className=\"rounded-md bg-red-50 dark:bg-red-900 dark:bg-opacity-50 p-4 not-prose\">\n\t\t\t\t<div className=\"flex items-center justify-center\">\n\t\t\t\t\t<div className=\"flex-shrink-0\">\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\tclassName=\"h-5 w-5 text-red-400 dark:text-red-500\"\n\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\t\t\t\t\td=\"M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z\"\n\t\t\t\t\t\t\t\tclipRule=\"evenodd\"\n\t\t\t\t\t\t\t></path>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"ml-3 flex-1 md:flex md:justify-between\">\n\t\t\t\t\t\t<p className=\"text-sm text-red-700 dark:text-red-400\">The NFT embed failed to load</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (isLoading) return <Loading />\n\n\treturn <NFT data={data} />\n}\n\nexport default Embed\n"
  },
  {
    "path": "src/components/OpenGraph.tsx",
    "content": "'use client'\n\nimport useSWR from 'swr'\nimport EntryLink from './EntryLink'\nimport { FC, PropsWithChildren } from 'react'\nimport { Metadata } from '@/types/link-preview'\nimport { LinkIcon } from '@heroicons/react/solid'\n\nconst OpenGraph: FC<PropsWithChildren<{ url: string }>> = ({ url, children }) => {\n\tconst { data, error, isLoading } = useSWR<Metadata>(`/api/link-preview?url=${url}`, url =>\n\t\tfetch(url).then(res => res.json())\n\t)\n\n\tif (error) return <EntryLink href={url}>{children}</EntryLink>\n\n\treturn (\n\t\t<figure\n\t\t\tclassName={`rounded-lg max-w-lg mx-auto shadow-card bg-white dark:bg-gray-800 opengraph overflow-hidden ${\n\t\t\t\tisLoading ? '' : 'hover:ring-4 transition duration-300 ring-gray-300 dark:ring-gray-700'\n\t\t\t}`}\n\t\t>\n\t\t\t{isLoading ? (\n\t\t\t\t<div className=\"py-16 px-8 flex items-center justify-center\">\n\t\t\t\t\t<svg\n\t\t\t\t\t\tclassName=\"w-6 h-6 animate-spin text-black dark:text-white text-opacity-40\"\n\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<circle\n\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\tstrokeDasharray=\"32\"\n\t\t\t\t\t\t\tcx=\"12\"\n\t\t\t\t\t\t\tcy=\"12\"\n\t\t\t\t\t\t\tr=\"10\"\n\t\t\t\t\t\t></circle>\n\t\t\t\t\t\t<circle\n\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\tcx=\"12\"\n\t\t\t\t\t\t\tcy=\"12\"\n\t\t\t\t\t\t\tr=\"10\"\n\t\t\t\t\t\t\topacity=\"0.25\"\n\t\t\t\t\t\t></circle>\n\t\t\t\t\t</svg>\n\t\t\t\t</div>\n\t\t\t) : (\n\t\t\t\t<EntryLink className=\"block\" href={url}>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t{data!.image && (\n\t\t\t\t\t\t\t<div className=\"pb-[50%] relative border-b dark:border-gray-700\">\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\talt={data!.title}\n\t\t\t\t\t\t\t\t\tsrc={data!.image.url}\n\t\t\t\t\t\t\t\t\tclassName=\"object-cover absolute inset-0 h-full w-full\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<div className=\"py-5 px-5\">\n\t\t\t\t\t\t\t<p className=\"text-gray-800 dark:text-gray-200 font-medium break-words text-base\">\n\t\t\t\t\t\t\t\t{data!.title}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p className=\"text-gray-400 dark:text-gray-500 text-base !mt-1\">{data!.description}</p>\n\t\t\t\t\t\t\t<div className=\"flex items-center space-x-2 !mt-3\">\n\t\t\t\t\t\t\t\t<LinkIcon className=\"w-4 h-4 text-gray-400 dark:text-gray-600\" />\n\t\t\t\t\t\t\t\t<p className=\"text-gray-500 dark:text-gray-600 text-base\">\n\t\t\t\t\t\t\t\t\t{new URL(data!.url!).hostname}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</EntryLink>\n\t\t\t)}\n\t\t</figure>\n\t)\n}\n\nexport default OpenGraph\n"
  },
  {
    "path": "src/context/image_sizes.tsx",
    "content": "'use client'\n\nimport { createContext, useContext, FC, PropsWithChildren } from 'react'\nimport { ISizeCalculationResult } from 'image-size/dist/types/interface'\n\nconst ImageSizeProvider = createContext<Record<string, ISizeCalculationResult> | null>(null)\nImageSizeProvider.displayName = 'ImageSizeProvider'\n\nexport default ImageSizeProvider\n\nexport const useImageSizes = (): Record<string, ISizeCalculationResult> => {\n\treturn useContext(ImageSizeProvider)!\n}\n\ntype Props = PropsWithChildren<{ sizes: Record<string, ISizeCalculationResult> }>\nexport const SetImageSizes: FC<Props> = ({ children, sizes }) => {\n\treturn <ImageSizeProvider.Provider value={sizes}>{children}</ImageSizeProvider.Provider>\n}\n"
  },
  {
    "path": "src/context/theme.tsx",
    "content": "'use client'\n\nimport { createContext, useContext, FC, PropsWithChildren } from 'react'\n\nconst ThemeProvider = createContext<{ colorMode: 'DARK' | 'LIGHT'; accent: string } | null>(null)\nThemeProvider.displayName = 'ThemeProvider'\n\nexport default ThemeProvider\n\nexport const useTheme = (): { colorMode: 'DARK' | 'LIGHT'; accent: string } => {\n\treturn useContext(ThemeProvider)!\n}\n\ntype Props = PropsWithChildren<{ theme: { colorMode: 'DARK' | 'LIGHT'; accent: string } }>\nexport const SetTheme: FC<Props> = ({ children, theme }) => {\n\treturn <ThemeProvider.Provider value={theme}>{children}</ThemeProvider.Provider>\n}\n"
  },
  {
    "path": "src/data/ERC721.ts",
    "content": "export default [\n\t{\n\t\tanonymous: false,\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'owner',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'approved',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'Approval',\n\t\ttype: 'event',\n\t},\n\t{\n\t\tanonymous: false,\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'owner',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'operator',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tindexed: false,\n\t\t\t\tinternalType: 'bool',\n\t\t\t\tname: 'approved',\n\t\t\t\ttype: 'bool',\n\t\t\t},\n\t\t],\n\t\tname: 'ApprovalForAll',\n\t\ttype: 'event',\n\t},\n\t{\n\t\tanonymous: false,\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'from',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'to',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tindexed: true,\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'Transfer',\n\t\ttype: 'event',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'to',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'approve',\n\t\toutputs: [],\n\t\tstateMutability: 'nonpayable',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'owner',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t],\n\t\tname: 'balanceOf',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'balance',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'getApproved',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'operator',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'owner',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'operator',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t],\n\t\tname: 'isApprovedForAll',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'bool',\n\t\t\t\tname: '',\n\t\t\t\ttype: 'bool',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [],\n\t\tname: 'name',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'string',\n\t\t\t\tname: '',\n\t\t\t\ttype: 'string',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'ownerOf',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'owner',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'from',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'to',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'safeTransferFrom',\n\t\toutputs: [],\n\t\tstateMutability: 'nonpayable',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'from',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'to',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'bytes',\n\t\t\t\tname: 'data',\n\t\t\t\ttype: 'bytes',\n\t\t\t},\n\t\t],\n\t\tname: 'safeTransferFrom',\n\t\toutputs: [],\n\t\tstateMutability: 'nonpayable',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'operator',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'bool',\n\t\t\t\tname: '_approved',\n\t\t\t\ttype: 'bool',\n\t\t\t},\n\t\t],\n\t\tname: 'setApprovalForAll',\n\t\toutputs: [],\n\t\tstateMutability: 'nonpayable',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'bytes4',\n\t\t\t\tname: 'interfaceId',\n\t\t\t\ttype: 'bytes4',\n\t\t\t},\n\t\t],\n\t\tname: 'supportsInterface',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'bool',\n\t\t\t\tname: '',\n\t\t\t\ttype: 'bool',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [],\n\t\tname: 'symbol',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'string',\n\t\t\t\tname: '',\n\t\t\t\ttype: 'string',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'index',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'tokenByIndex',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: '',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'owner',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'index',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'tokenOfOwnerByIndex',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'tokenURI',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'string',\n\t\t\t\tname: '',\n\t\t\t\ttype: 'string',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [],\n\t\tname: 'totalSupply',\n\t\toutputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: '',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tstateMutability: 'view',\n\t\ttype: 'function',\n\t},\n\t{\n\t\tinputs: [\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'from',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'address',\n\t\t\t\tname: 'to',\n\t\t\t\ttype: 'address',\n\t\t\t},\n\t\t\t{\n\t\t\t\tinternalType: 'uint256',\n\t\t\t\tname: 'tokenId',\n\t\t\t\ttype: 'uint256',\n\t\t\t},\n\t\t],\n\t\tname: 'transferFrom',\n\t\toutputs: [],\n\t\tstateMutability: 'nonpayable',\n\t\ttype: 'function',\n\t},\n] as const\n"
  },
  {
    "path": "src/data/ens.ts",
    "content": "export const contributorAddresses = ['0xE340b00B6B622C136fFA5CFf130eC8edCdDCb39D']\n"
  },
  {
    "path": "src/data/entries.ts",
    "content": "import slug from 'slug'\nimport { cache } from 'react'\nimport arweave from '@/lib/arweave'\nimport { arweaveQL } from '@/lib/graphql'\nimport { contributorAddresses } from './ens'\nimport { calculateSizes } from '@/utils/images'\nimport fetchTransactions from '@/queries/arweave/fetch-transactions'\nimport { ISizeCalculationResult } from 'image-size/dist/types/interface'\nimport fetchSingleTransaction from '@/queries/arweave/fetch-single-transaction'\n\nexport type Entry = {\n\ttitle: string\n\tslug: string\n\tbody: string\n\ttimestamp: number\n\tdigest: string\n\tcontributor: string\n\ttransaction: string\n\tcover_image: string\n\timage_sizes: Record<string, ISizeCalculationResult>\n}\n\nexport const getEntryPaths = async () => {\n\tconst {\n\t\tdata: {\n\t\t\ttransactions: { edges },\n\t\t},\n\t} = await arweaveQL.query({ query: fetchTransactions, variables: { addresses: contributorAddresses } })\n\n\treturn edges\n\t\t.map(({ node }) => {\n\t\t\tconst tags = Object.fromEntries(node.tags.map(tag => [tag.name, tag.value]))\n\n\t\t\treturn { slug: tags['Original-Content-Digest'], path: node.id, timestamp: node.block.timestamp }\n\t\t})\n\t\t.filter(entry => entry.slug && entry.slug !== '')\n\t\t.reduce((acc, current) => {\n\t\t\tconst x = acc.findIndex(entry => entry.slug === current.slug)\n\t\t\tif (x == -1) return acc.concat([current])\n\t\t\telse {\n\t\t\t\tacc[x].timestamp = current.timestamp\n\n\t\t\t\treturn acc\n\t\t\t}\n\t\t}, [])\n}\n\nconst getEntries = async () => {\n\tconst paths = await getEntryPaths()\n\n\treturn (\n\t\tawait Promise.all(\n\t\t\tpaths.map(async entry =>\n\t\t\t\tformatEntry(\n\t\t\t\t\tJSON.parse(\n\t\t\t\t\t\t(await arweave.transactions.getData(entry.path, { decode: true, string: true })) as string\n\t\t\t\t\t),\n\t\t\t\t\tentry.slug,\n\t\t\t\t\tentry.timestamp\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t)\n\t\t.sort((a, b) => b.timestamp - a.timestamp)\n\t\t.reduce((acc, current) => {\n\t\t\tconst x = acc.find(entry => entry.slug === current.slug)\n\t\t\tif (!x) return acc.concat([current])\n\t\t\telse return acc\n\t\t}, [])\n}\n\nexport const getEntry = async (digest: string): Promise<Entry | null> => {\n\tconst {\n\t\tdata: {\n\t\t\ttransactions: { edges },\n\t\t},\n\t} = await arweaveQL.query({ query: fetchSingleTransaction, variables: { digest } })\n\n\tif (edges.length === 0) return null\n\n\tconst {\n\t\t0: {\n\t\t\tnode: { id: transactionId, block },\n\t\t},\n\t} = edges\n\n\treturn formatEntry(\n\t\tJSON.parse((await arweave.transactions.getData(transactionId, { decode: true, string: true })) as string),\n\t\ttransactionId,\n\t\tblock?.timestamp ?? Date.now() / 1000,\n\t\tdigest\n\t)\n}\n\nconst formatEntry = async (entry, transactionId, timestamp, digest?: string): Promise<Entry> => ({\n\ttitle: entry.content.title,\n\tslug: slug(entry.content.title),\n\tbody: entry.content.body,\n\ttimestamp: timestamp,\n\tdigest: digest ?? entry.originalDigest ?? entry.digest,\n\tcontributor: entry.authorship.contributor,\n\ttransaction: transactionId,\n\tcover_image:\n\t\t(entry.content.body.split('\\n\\n')[0].match(/!\\[[^\\]]*\\]\\((.*?)\\s*(\"(?:.*[^\"])\")?\\s*\\)/m) || [])?.[1] || null,\n\timage_sizes: await calculateSizes(entry.content.body),\n})\n\nexport default cache(getEntries)\n"
  },
  {
    "path": "src/data/publication.ts",
    "content": "import { cache } from 'react'\nimport { mirrorQL } from '@/lib/graphql'\nimport getConfig from '@/hooks/getConfig'\nimport fetchPublication from '@/queries/mirror/fetch-publication'\n\nexport type Publication = {\n\tens: string\n\tdomain: string\n\tavatarURL: string\n\tdisplayName: string\n\theaderImage?: { url: string }\n\ttheme: { colorMode: 'DARK' | 'LIGHT'; accent: string }\n\tdescription: string\n\tmailingListURL?: string\n\tmembers: Array<{\n\t\taddress: string\n\t\tavatarURL: string\n\t\tdisplayName: string\n\t}>\n}\n\nconst getPublication = async (): Promise<Publication> => {\n\tconst { publicationAddress } = getConfig()\n\n\tconst {\n\t\tdata: { projectFeed: publication },\n\t} = await mirrorQL.query({ query: fetchPublication, variables: { publicationAddress } })\n\n\treturn publication\n}\n\nexport default cache(getPublication)\n"
  },
  {
    "path": "src/hooks/getConfig.ts",
    "content": "import { contributorAddresses } from '@/data/ens'\n\nconst getConfig = (): { ensDomain: string; publicationAddress: string } => {\n\treturn { ensDomain: process.env.NEXT_PUBLIC_AUTHOR_ENS!, publicationAddress: contributorAddresses[0] }\n}\n\nexport default getConfig\n"
  },
  {
    "path": "src/lib/arweave.ts",
    "content": "import Arweave from 'arweave'\n\nconst arweave = Arweave.init({ host: 'arweave.net', protocol: 'https', port: 443, timeout: 5000 })\n\nexport default arweave\n"
  },
  {
    "path": "src/lib/graphql.ts",
    "content": "import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'\n\nexport const mirrorQL = new ApolloClient({\n\tlink: new HttpLink({ uri: 'https://mirror.xyz/api/graphql', fetch, headers: { origin: 'https://mirror.xyz' } }),\n\tcache: new InMemoryCache(),\n})\n\nexport const arweaveQL = new ApolloClient({\n\turi: 'https://arweave.net/graphql',\n\tcache: new InMemoryCache(),\n})\n"
  },
  {
    "path": "src/queries/arweave/fetch-single-transaction.ts",
    "content": "import { gql } from '@apollo/client'\n\nexport default gql`\n\tquery FetchTransaction($digest: String!) {\n\t\ttransactions(\n\t\t\ttags: [{ name: \"Original-Content-Digest\", values: [$digest] }, { name: \"App-Name\", values: \"MirrorXYZ\" }]\n\t\t) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\tid\n\t\t\t\t\tblock {\n\t\t\t\t\t\ttimestamp\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n`\n"
  },
  {
    "path": "src/queries/arweave/fetch-transactions.ts",
    "content": "import { gql } from '@apollo/client'\n\nexport default gql`\n\tquery FetchTransactions($addresses: [String!]!) {\n\t\ttransactions(\n\t\t\tfirst: 100\n\t\t\ttags: [{ name: \"App-Name\", values: [\"MirrorXYZ\"] }, { name: \"Contributor\", values: $addresses }]\n\t\t) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\tid\n\t\t\t\t\tblock {\n\t\t\t\t\t\ttimestamp\n\t\t\t\t\t}\n\t\t\t\t\ttags {\n\t\t\t\t\t\tname\n\t\t\t\t\t\tvalue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n`\n"
  },
  {
    "path": "src/queries/mirror/fetch-publication.ts",
    "content": "import { gql } from '@apollo/client'\n\nexport default gql`\n\tquery PublicationInfo($publicationAddress: String!) {\n\t\tprojectFeed(projectAddress: $publicationAddress) {\n\t\t\tdisplayName\n\t\t\tavatarURL\n\t\t\tdomain\n\t\t\tens\n\t\t\theaderImage {\n\t\t\t\turl\n\t\t\t}\n\t\t\ttheme {\n\t\t\t\tcolorMode\n\t\t\t\taccent\n\t\t\t}\n\t\t\tdescription\n\t\t\tmailingListURL\n\t\t\tmembers {\n\t\t\t\taddress\n\t\t\t\tdisplayName\n\t\t\t\tavatarURL\n\t\t\t}\n\t\t}\n\t}\n`\n"
  },
  {
    "path": "src/styles/style.css",
    "content": "@import 'react-medium-image-zoom/dist/styles.css';\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbutton[aria-label='Zoom image'],\nbutton[aria-label='Unzoom image'] {\n\t@apply outline-none focus-visible:ring;\n}\n\n.nfte--loaded .nfte__media {\n\taspect-ratio: unset !important;\n}\n"
  },
  {
    "path": "src/types/link-preview.ts",
    "content": "import type { Metadata as MetascraperMetadata } from 'metascraper'\nimport type { ISizeCalculationResult } from 'image-size/dist/types/interface'\n\nexport type Metadata = MetascraperMetadata & {\n\tlogo?: { url: string } & ISizeCalculationResult\n\timage?: { url: string } & ISizeCalculationResult\n}\n"
  },
  {
    "path": "src/utils/address.ts",
    "content": "export const formatAddress = (address: string): string => {\n\tif (!address) return ''\n\n\tconst chars = address.split('')\n\treturn `${chars.slice(0, 6).join('')}…${chars.slice(-6).join('')}`\n}\n"
  },
  {
    "path": "src/utils/embeds.ts",
    "content": "import routeToBlock from 'react-embed/lib/routeToBlock'\n\nexport const shouldEmbed = (url: string): boolean => {\n\treturn routeToBlock({}, { url, ...new URL(url) }) !== undefined\n}\n"
  },
  {
    "path": "src/utils/excerpt.ts",
    "content": "export const getExcerpt = (content: string): string => {\n\tlet firstParagraphs = content.split('\\n\\n').slice(0, 4)\n\n\tif (firstParagraphs[firstParagraphs.length - 1].startsWith('#')) firstParagraphs.pop()\n\n\treturn firstParagraphs.join('\\n\\n')\n}\n"
  },
  {
    "path": "src/utils/highlightMarkdown.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { bundledLanguages, getHighlighter } from 'shiki'\n\nconst highlighter = getHighlighter({ langs: Object.keys(bundledLanguages), themes: ['github-dark', 'github-light'] })\n\nexport default ({ theme = 'light' }: { theme: 'light' | 'dark' }) =>\n\tasync tree => {\n\t\tawait highlighter.then(highlighter => {\n\t\t\tvisit(tree, 'code', node => {\n\t\t\t\tnode.type = 'html'\n\t\t\t\tnode.children = undefined\n\t\t\t\tnode.value = highlighter\n\t\t\t\t\t.codeToHtml(node.value, {\n\t\t\t\t\t\tlang: node.lang,\n\t\t\t\t\t\ttheme: theme === 'dark' ? 'github-dark' : 'github-light',\n\t\t\t\t\t})\n\t\t\t\t\t.replace(\n\t\t\t\t\t\t'<pre class=\"shiki',\n\t\t\t\t\t\t`<pre language=\"${node.lang}\" meta=\"${node.meta}\" class=\"border dark:border-transparent rounded-lg`\n\t\t\t\t\t)\n\t\t\t})\n\t\t})\n\t}\n"
  },
  {
    "path": "src/utils/images.ts",
    "content": "import sizeOf from 'image-size'\nimport { ISizeCalculationResult } from 'image-size/dist/types/interface'\n\nexport const calculateSizes = async (markdown: string): Promise<Record<string, ISizeCalculationResult>> => {\n\treturn Object.fromEntries(\n\t\tawait Promise.all(\n\t\t\t[...markdown.matchAll(/!\\[[^\\]]*\\]\\((?<url>.*?)\\s*(\"(?:.*[^\"])\")?\\s*\\)/gm)].map(async match => [\n\t\t\t\tmatch.groups!.url,\n\t\t\t\tawait calculateImageSize(match.groups!.url),\n\t\t\t])\n\t\t)\n\t)\n}\n\nexport const calculateImageSize = async (url: string): Promise<ISizeCalculationResult> => {\n\tconst image = await fetch(url).then(res => res.arrayBuffer())\n\n\treturn sizeOf(new Uint8Array(image))\n}\n"
  },
  {
    "path": "src/utils/markdown.tsx",
    "content": "'use client'\n\nimport Embed from 'react-embed'\nimport NextImage from 'next/image'\nimport NFT from '@/components/NFT'\nimport { shouldEmbed } from './embeds'\nimport getConfig from '@/hooks/getConfig'\nimport { useTheme } from '@/context/theme'\nimport Zoom from 'react-medium-image-zoom'\nimport OpenGraph from '@/components/OpenGraph'\nimport EntryLink from '@/components/EntryLink'\nimport { useImageSizes } from '@/context/image_sizes'\n\nexport const Image = ({ alt, src }) => {\n\tconst { colorMode } = useTheme()\n\tconst {\n\t\t[src]: { width, height },\n\t} = useImageSizes()\n\n\treturn (\n\t\t<figure>\n\t\t\t<Zoom\n\t\t\t\twrapElement=\"span\"\n\t\t\t\twrapStyle={{ width: '100%' }}\n\t\t\t\toverlayBgColorStart={colorMode == 'DARK' ? 'rgba(0, 0, 0, 0)' : 'rgba(255, 255, 255, 0)'}\n\t\t\t\toverlayBgColorEnd={colorMode == 'DARK' ? 'rgba(0, 0, 0, 0.95)' : 'rgba(255, 255, 255, 0.95)'}\n\t\t\t>\n\t\t\t\t<NextImage width={width} height={height} alt={alt} src={src} />\n\t\t\t</Zoom>\n\t\t\t{alt && <figcaption>{alt}</figcaption>}\n\t\t</figure>\n\t)\n}\n\nexport const LinkOrEmbed = ({ href, children, ...props }) => {\n\tconst { colorMode } = useTheme()\n\tconst { ensDomain } = getConfig()\n\n\tconst blockSize = props?.node?.blockSize\n\tif (blockSize != 1) return <EntryLink href={href}>{children}</EntryLink>\n\n\tconst parsedURL = new URL(href)\n\tif (parsedURL.protocol === 'nft:') {\n\t\tconst [chainId, contract, tokenId] = parsedURL.pathname.replace(/^\\/+/, '').split('/')\n\n\t\treturn <NFT chainId={chainId} contract={contract} tokenId={tokenId} />\n\t}\n\n\tif (parsedURL.protocol === 'crowdfund:') {\n\t\tconst [, crowdfundAddress] = href.match(/crowdfund:\\/\\/(\\w*)/m)\n\n\t\treturn <EntryLink href={`https://${ensDomain}.mirror.xyz/crowdfunds/${crowdfundAddress}`}>{children}</EntryLink>\n\t}\n\n\tif (typeof window !== 'undefined' && shouldEmbed(href)) {\n\t\treturn <Embed url={href} isDark={colorMode === 'DARK'} />\n\t}\n\n\treturn <OpenGraph url={href}>{children}</OpenGraph>\n}\n\nconst getClass = (accentColor: string): string => {\n\tswitch (accentColor.toLowerCase()) {\n\t\tcase 'purple':\n\t\t\treturn '!border-fuchsia-400'\n\t\tcase 'pink':\n\t\t\treturn '!border-red-500'\n\t\tcase 'red':\n\t\t\treturn '!border-red-500'\n\t\tcase 'orange':\n\t\t\treturn '!border-orange-400'\n\t\tcase 'yellow':\n\t\t\treturn '!border-yellow-400'\n\t\tcase 'teal':\n\t\t\treturn '!border-cyan-400'\n\t\tcase 'blue':\n\t\t\treturn '!border-blue-500'\n\t\tcase 'indigo':\n\t\t\treturn '!border-indigo-400'\n\t\tcase 'green':\n\t\t\treturn '!border-emerald-400'\n\t\tcase 'foreground':\n\t\t\treturn '!border-white'\n\n\t\tdefault:\n\t\t\treturn '!border-blue-400'\n\t}\n}\n\nexport const BlockQuote = ({ children }) => {\n\tconst { accent } = useTheme()\n\n\treturn <blockquote className={getClass(accent)}>{children}</blockquote>\n}\n\nexport const Block = ({ children }) => {\n\tchildren = Array.isArray(children) ? children : [children]\n\n\tconst blockAwareChildren = children.map(child => {\n\t\tif (child?.props?.node) child.props.node.blockSize = children.length\n\n\t\treturn child\n\t})\n\n\treturn <p>{blockAwareChildren}</p>\n}\n"
  },
  {
    "path": "src/utils/url.ts",
    "content": "const allowedLinkProtocols = ['http', 'https', 'mailto', 'tel', 'crowdfund', 'nft']\n\nexport const uriTransformer = (uri: string): string => {\n\tconst url = (uri || '').trim()\n\tconst first = url.charAt(0)\n\n\tif (first === '#' || first === '/') {\n\t\treturn url\n\t}\n\n\tconst colon = url.indexOf(':')\n\tif (colon === -1) {\n\t\treturn url\n\t}\n\n\tconst length = allowedLinkProtocols.length\n\tlet index = -1\n\n\twhile (++index < length) {\n\t\tconst protocol = allowedLinkProtocols[index]\n\n\t\tif (colon === protocol.length && url.slice(0, protocol.length).toLowerCase() === protocol) {\n\t\t\treturn url\n\t\t}\n\t}\n\n\tindex = url.indexOf('?')\n\tif (index !== -1 && colon > index) {\n\t\treturn url\n\t}\n\n\tindex = url.indexOf('#')\n\tif (index !== -1 && colon > index) {\n\t\treturn url\n\t}\n\n\treturn '#'\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "import colors from 'tailwindcss/colors'\nimport defaultTheme from 'tailwindcss/defaultTheme'\n\nexport default {\n\tcontent: ['./src/**/*.{js,ts,jsx,tsx}'],\n\tdarkMode: 'class',\n\ttheme: {\n\t\textend: {\n\t\t\tcolors: {\n\t\t\t\tgray: colors.neutral,\n\t\t\t\tfuchsia: colors.fuchsia,\n\t\t\t\torange: colors.orange,\n\t\t\t\tcyan: colors.cyan,\n\t\t\t\temerald: colors.emerald,\n\t\t\t},\n\t\t\tfontFamily: {\n\t\t\t\tsans: ['var(--font-inter)', ...defaultTheme.fontFamily.sans],\n\t\t\t\tmono: ['iAWriter Mono', ...defaultTheme.fontFamily.mono],\n\t\t\t},\n\t\t\tboxShadow: {\n\t\t\t\tcard: '0 0 0.5rem rgba(0, 0, 0, 0.075)',\n\t\t\t},\n\t\t\ttypography: theme => ({\n\t\t\t\tDEFAULT: {\n\t\t\t\t\tcss: {\n\t\t\t\t\t\twhiteSpace: 'pre-wrap',\n\t\t\t\t\t\t'> *, blockquote > *': {\n\t\t\t\t\t\t\tmarginTop: '0 !important',\n\t\t\t\t\t\t\tmarginBottom: '0 !important',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfigcaption: {\n\t\t\t\t\t\t\ttextAlign: 'center',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tstrong: {\n\t\t\t\t\t\t\tfontWeight: theme('fontWeight.medium'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\timg: {\n\t\t\t\t\t\t\tmarginTop: null,\n\t\t\t\t\t\t\tmarginBottom: null,\n\t\t\t\t\t\t\tborderRadius: theme('borderRadius.lg'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tblockquote: {\n\t\t\t\t\t\t\tdisplay: 'flex',\n\t\t\t\t\t\t\tfontStyle: null,\n\t\t\t\t\t\t\tborderLeftWidth: '2px',\n\t\t\t\t\t\t\tborderColor: theme('colors.blue.600'),\n\t\t\t\t\t\t\tfontWeight: theme('fontWeight.normal'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\thr: {\n\t\t\t\t\t\t\tborderTopWidth: '2px',\n\t\t\t\t\t\t\tmaxWidth: '12rem',\n\t\t\t\t\t\t\tmarginLeft: 'auto',\n\t\t\t\t\t\t\tmarginRight: 'auto',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'blockquote p:first-of-type::before, blockquote p:last-of-type::after': {\n\t\t\t\t\t\t\tcontent: 'unset !important',\n\t\t\t\t\t\t},\n\t\t\t\t\t\ta: {\n\t\t\t\t\t\t\tcolor: theme('colors.blue.600'),\n\t\t\t\t\t\t\twordWrap: 'break-word',\n\t\t\t\t\t\t\tfontWeight: theme('fontWeight.normal'),\n\t\t\t\t\t\t\ttextDecoration: 'none',\n\t\t\t\t\t\t\ttextUnderlineOffset: '0.2em',\n\t\t\t\t\t\t\t'&:hover': {\n\t\t\t\t\t\t\t\ttextDecoration: 'underline',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'ol > li::before': {\n\t\t\t\t\t\t\tfontFamily: theme('fontFamily.mono').join(', '),\n\t\t\t\t\t\t\tcolor: theme('colors.gray.400'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'ul > li::before': {\n\t\t\t\t\t\t\tbackgroundColor: theme('colors.gray.400'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'.twitter-tweet': {\n\t\t\t\t\t\t\tmarginLeft: 'auto',\n\t\t\t\t\t\t\tmarginRight: 'auto',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcode: {\n\t\t\t\t\t\t\tfontWeight: theme('fontWeight.normal'),\n\t\t\t\t\t\t\tbackground: theme('colors.gray.200'),\n\t\t\t\t\t\t\tcolor: theme('colors.gray.600'),\n\t\t\t\t\t\t\tborderRadius: theme('borderRadius.lg'),\n\t\t\t\t\t\t\tpadding: `0.125rem ${theme('padding.1')}`,\n\t\t\t\t\t\t\t'&::before, &::after': {\n\t\t\t\t\t\t\t\tcontent: 'unset !important',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'[data-nft] *, .opengraph *, .opengraph *:hover': {\n\t\t\t\t\t\t\tmargin: '0',\n\t\t\t\t\t\t\ttextDecoration: 'none',\n\t\t\t\t\t\t\tborderRadius: 'unset',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdark: {\n\t\t\t\t\tcss: {\n\t\t\t\t\t\tcolor: theme('colors.gray.300'),\n\t\t\t\t\t\tstrong: {\n\t\t\t\t\t\t\tcolor: theme('colors.gray.300'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'h1, h2, h3, h4, h5, h6': {\n\t\t\t\t\t\t\tcolor: theme('colors.gray.200'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tblockquote: {\n\t\t\t\t\t\t\tcolor: theme('colors.gray.300'),\n\t\t\t\t\t\t\tborderColor: theme('colors.yellow.400'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'ol > li::before': {\n\t\t\t\t\t\t\tcolor: theme('colors.gray.500'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'ul > li::before': {\n\t\t\t\t\t\t\tbackgroundColor: theme('colors.gray.600'),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t':not(pre) > code': {\n\t\t\t\t\t\t\tbackground: theme('colors.gray.800'),\n\t\t\t\t\t\t\tcolor: 'unset',\n\t\t\t\t\t\t},\n\t\t\t\t\t\thr: {\n\t\t\t\t\t\t\tborderColor: theme('colors.gray.700'),\n\t\t\t\t\t\t\topacity: '0.6',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlg: {\n\t\t\t\t\tcss: {\n\t\t\t\t\t\t'ol > li, ul > li': {\n\t\t\t\t\t\t\tmarginTop: '0',\n\t\t\t\t\t\t\tmarginBottom: '0',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Image margin is handled by `figure`\n\t\t\t\t\t\timg: {\n\t\t\t\t\t\t\tmarginTop: null,\n\t\t\t\t\t\t\tmarginBottom: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t},\n\tplugins: [require('@tailwindcss/typography')],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n\t\t\"allowJs\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"strict\": false,\n\t\t\"noEmit\": true,\n\t\t\"incremental\": true,\n\t\t\"module\": \"ESNext\",\n\t\t\"target\": \"ESNext\",\n\t\t\"esModuleInterop\": true,\n\t\t\"moduleResolution\": \"node\",\n\t\t\"resolveJsonModule\": true,\n\t\t\"isolatedModules\": true,\n\t\t\"jsx\": \"preserve\",\n\t\t\"baseUrl\": \".\",\n\t\t\"paths\": {\n\t\t\t\"@/*\": [\"src/*\"]\n\t\t},\n\t\t\"plugins\": [\n\t\t\t{\n\t\t\t\t\"name\": \"next\"\n\t\t\t}\n\t\t],\n\t\t\"strictNullChecks\": true\n\t},\n\t\"include\": [\n\t\t\"next-env.d.ts\",\n\t\t\"**/*.ts\",\n\t\t\"**/*.tsx\",\n\t\t\".next/types/**/*.ts\",\n\t\t\"src/scripts/resolve-ens.js\",\n\t\t\"next.config.js\"\n\t],\n\t\"exclude\": [\"node_modules\"]\n}\n"
  }
]