├── .travis.yml ├── pages ├── 404.tsx ├── _error.tsx ├── index.tsx ├── test.tsx ├── _app.tsx └── _document.tsx ├── .prettierignore ├── next-env.d.ts ├── public ├── 404.png ├── error.png ├── favicon.ico ├── favicon.png ├── social.jpg ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── default-avatar.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── manifest.json └── noflash.js ├── components ├── index.ts ├── LoadingIndicator │ ├── styles.module.css │ └── LoadingIndicator.tsx ├── Paper │ ├── styles.module.css │ └── Paper.tsx ├── Page404.tsx ├── ErrorPage.tsx ├── InlineTweet │ ├── styles.module.css │ ├── Twemoji.tsx │ └── InlineTweet.tsx ├── QueryParamProvider.tsx ├── TweetIndexSearch │ ├── styles.module.css │ └── TweetIndexSearch.tsx ├── styles.module.css ├── GitHubShareButton.tsx └── App.tsx ├── media ├── favicon.ico ├── screenshot-search-ui-0.jpg ├── logo.svg └── icons │ ├── search.svg │ ├── cluster.svg │ ├── fast.svg │ ├── customizable.svg │ ├── sync.svg │ ├── open-source.svg │ ├── logs.svg │ ├── stripe.svg │ └── global.svg ├── .editorconfig ├── lib ├── client │ ├── algolia.js │ ├── sdk.ts │ └── bootstrap.ts ├── server │ ├── algolia.ts │ ├── twitter.ts │ └── sync.ts └── types.ts ├── next.config.js ├── .prettierrc ├── tsoa.json ├── styles └── global.css ├── .gitignore ├── api ├── get-index.ts ├── sync-index.ts └── get-tweet-ast │ └── [tweetId].ts ├── .env.example ├── tsconfig.json ├── license ├── readme.md ├── .eslintrc └── package.json /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - 12 5 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Page404 } from 'components' 2 | 3 | export default Page404 4 | -------------------------------------------------------------------------------- /pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorPage } from 'components' 2 | 3 | export default ErrorPage 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .snapshots/ 2 | build/ 3 | dist/ 4 | node_modules/ 5 | .next/ 6 | .vercel/ 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /public/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/404.png -------------------------------------------------------------------------------- /public/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/error.png -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Page404' 2 | export * from './ErrorPage' 3 | export * from './App' 4 | -------------------------------------------------------------------------------- /media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/media/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/social.jpg -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/default-avatar.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /media/screenshot-search-ui-0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/media/screenshot-search-ui-0.jpg -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/twitter-search/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { App } from 'components' 2 | 3 | export const getStaticProps = async () => { 4 | return { 5 | props: {}, 6 | revalidate: 10 7 | } 8 | } 9 | 10 | export default App 11 | -------------------------------------------------------------------------------- /lib/client/algolia.js: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch/lite' 2 | 3 | export const searchClient = algoliasearch( 4 | process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID, 5 | process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY 6 | ) 7 | -------------------------------------------------------------------------------- /lib/server/algolia.ts: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch' 2 | export * from 'algoliasearch' 3 | 4 | export const client = algoliasearch( 5 | process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID, 6 | process.env.ALGOLIA_SECRET_KEY 7 | ) 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true' 3 | }) 4 | 5 | module.exports = withBundleAnalyzer({ 6 | images: { 7 | domains: ['pbs.twimg.com'] 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none" 11 | } 12 | -------------------------------------------------------------------------------- /components/LoadingIndicator/styles.module.css: -------------------------------------------------------------------------------- 1 | .loadingIndicator { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | bottom: 0; 6 | right: 0; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | -------------------------------------------------------------------------------- /components/Paper/styles.module.css: -------------------------------------------------------------------------------- 1 | .paper { 2 | position: relative; 3 | padding: 24px; 4 | 5 | background: #fff; 6 | border-radius: 4px; 7 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2), 8 | 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.12); 9 | } 10 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface HttpBody { 2 | [key: string]: any 3 | } 4 | 5 | export interface HttpQuery { 6 | [key: string]: string 7 | } 8 | 9 | export interface HttpHeaders { 10 | [key: string]: string 11 | } 12 | 13 | export interface TweetIndex { 14 | indexName: string 15 | exists: boolean 16 | } 17 | -------------------------------------------------------------------------------- /tsoa.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryFile": "./src/server.ts", 3 | "noImplicitAdditionalProperties": "throw-on-extras", 4 | "spec": { 5 | "specVersion": 3, 6 | "outputDirectory": "dist" 7 | }, 8 | "routes": { 9 | "routesDir": "./src", 10 | "middleware": "koa" 11 | }, 12 | "ignore": ["**/node_modules/**"] 13 | } 14 | -------------------------------------------------------------------------------- /lib/server/twitter.ts: -------------------------------------------------------------------------------- 1 | import Twitter from 'twitter-lite' 2 | 3 | export const twitterClient = new Twitter({ 4 | consumer_key: process.env.TWITTER_CONSUMER_KEY, 5 | consumer_secret: process.env.TWITTER_CONSUMER_SECRET, 6 | access_token_key: process.env.TWITTER_ACCESS_TOKEN, 7 | access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET 8 | }) 9 | -------------------------------------------------------------------------------- /lib/client/sdk.ts: -------------------------------------------------------------------------------- 1 | export async function getIndex() { 2 | return fetch('/api/get-index').then((res) => res.json()) 3 | } 4 | 5 | export async function syncIndex() { 6 | return fetch('/api/sync-index', { 7 | method: 'PUT', 8 | headers: { 9 | 'Content-Type': 'application/json' 10 | } 11 | }).then((res) => res.json()) 12 | } 13 | -------------------------------------------------------------------------------- /components/Paper/Paper.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import cs from 'classnames' 3 | 4 | import styles from './styles.module.css' 5 | 6 | export class Paper extends Component { 7 | render() { 8 | const { className, ...rest } = this.props 9 | 10 | return
11 | } 12 | } 13 | -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | a { 6 | color: inherit; 7 | text-decoration: none; 8 | } 9 | 10 | body, 11 | html { 12 | padding: 0; 13 | margin: 0; 14 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 15 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 16 | } 17 | 18 | body { 19 | overflow-x: hidden; 20 | } 21 | 22 | img, 23 | svg { 24 | display: inline-block !important; 25 | } 26 | -------------------------------------------------------------------------------- /components/LoadingIndicator/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import cs from 'classnames' 3 | import { Spinner } from '@chakra-ui/core' 4 | 5 | import styles from './styles.module.css' 6 | 7 | export class LoadingIndicator extends Component { 8 | render() { 9 | const { className, ...rest } = this.props 10 | 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage/ 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | 15 | # production 16 | build/ 17 | dist/ 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env.local 31 | .env.build 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # vercel 37 | .vercel 38 | -------------------------------------------------------------------------------- /api/get-index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | import { getIndex } from '../lib/server/sync' 4 | 5 | export default async ( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ): Promise => { 9 | console.log('getTweetIndex') 10 | 11 | if (req.method !== 'GET') { 12 | return res.status(405).send({ error: 'method not allowed' }) 13 | } 14 | 15 | const index = await getIndex() 16 | const exists = await index.exists() 17 | 18 | res.status(200).json({ 19 | indexName: index.indexName, 20 | exists 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /components/Page404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './styles.module.css' 4 | 5 | export const Page404: React.FC<{ error?: { message: string } }> = ({ 6 | error 7 | }) => { 8 | return ( 9 | <> 10 |
11 |
12 |

Page Not Found

13 | 14 | {error &&

{error.message}

} 15 | 16 | 404 Not Found 21 |
22 |
23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # This is an example .env file. 3 | # 4 | # All of these environment vars must be defined either in your environment or in 5 | # a local .env file in order to run this app. 6 | # 7 | # @see https://github.com/rolodato/dotenv-safe 8 | # ------------------------------------------------------------------------------ 9 | 10 | NEXT_PUBLIC_ALGOLIA_APPLICATION_ID= 11 | NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY= 12 | 13 | ALGOLIA_SECRET_KEY= 14 | ALGOLA_INDEX_NAME= 15 | 16 | TWITTER_CONSUMER_KEY= 17 | TWITTER_CONSUMER_SECRET= 18 | 19 | TWITTER_ACCESS_TOKEN= 20 | TWITTER_ACCESS_TOKEN_SECRET= 21 | -------------------------------------------------------------------------------- /api/sync-index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | import { twitterClient } from '../lib/server/twitter' 4 | import { getIndex, syncAccount } from '../lib/server/sync' 5 | 6 | export default async ( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ): Promise => { 10 | console.log('syncTweetIndex') 11 | 12 | if (req.method !== 'PUT') { 13 | return res.status(405).send({ error: 'method not allowed' }) 14 | } 15 | 16 | const { full = false } = req.body 17 | 18 | const index = await getIndex() 19 | await syncAccount(twitterClient, index, full) 20 | 21 | res.status(200).json({ 22 | indexName: index.indexName, 23 | exists: true 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /components/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | 4 | import styles from './styles.module.css' 5 | 6 | export const ErrorPage: React.FC<{ statusCode?: number }> = ({ 7 | statusCode 8 | }) => { 9 | const title = 'Error' 10 | 11 | return ( 12 | <> 13 | 14 | {title} 15 | 16 | 17 |
18 |
19 |

Error Loading Page

20 | 21 | {statusCode &&

Error code: {statusCode}

} 22 | 23 | Error 24 |
25 |
26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /pages/test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { fetchTweetAst } from 'static-tweets' 3 | import { Tweet } from 'react-static-tweets' 4 | 5 | // const tweetId = '1238918791947522049' 6 | // const tweetId = '1358199505280262150' 7 | // const tweetId = '1358581276576391172' 8 | const tweetId = '1358199505280262150' 9 | 10 | export const getStaticProps = async () => { 11 | try { 12 | const tweetAst = await fetchTweetAst(tweetId) 13 | 14 | return { 15 | props: { 16 | tweetId, 17 | tweetAst 18 | }, 19 | revalidate: 10 20 | } 21 | } catch (err) { 22 | console.error('error fetching tweet info', tweetId, err) 23 | 24 | throw err 25 | } 26 | } 27 | 28 | export default function TweetTestPage({ tweetId, tweetAst }) { 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | // global styles shared across the entire site 2 | import 'styles/global.css' 3 | 4 | // core styles shared by all of react-static-tweets (required) 5 | import 'react-static-tweets/styles.css' 6 | 7 | import React from 'react' 8 | import Head from 'next/head' 9 | 10 | import { bootstrap } from 'lib/client/bootstrap' 11 | 12 | if (typeof window !== 'undefined') { 13 | bootstrap() 14 | } 15 | 16 | export default function App({ Component, pageProps }) { 17 | return ( 18 | <> 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "esnext", 5 | "allowJs": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "esModuleInterop": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "baseUrl": ".", 16 | "lib": ["esnext", "dom"], 17 | "typeRoots": ["./node_modules/@types"], 18 | "target": "es2016", 19 | "skipLibCheck": true, 20 | "strict": false, 21 | "noEmit": true 22 | }, 23 | "exclude": ["node_modules"], 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx", 28 | "static-tweet/lib/twitter/embed/tweet-html.js" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /lib/client/bootstrap.ts: -------------------------------------------------------------------------------- 1 | export function bootstrap() { 2 | console.log(` 3 | 4 | ████████╗██████╗ █████╗ ███╗ ██╗███████╗██╗████████╗██╗██╗ ██╗███████╗ ██████╗ ███████╗ 5 | ╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║╚══██╔══╝██║██║ ██║██╔════╝ ██╔══██╗██╔════╝ 6 | ██║ ██████╔╝███████║██╔██╗ ██║███████╗██║ ██║ ██║██║ ██║█████╗ ██████╔╝███████╗ 7 | ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║██║ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║ 8 | ██║ ██║ ██║██║ ██║██║ ╚████║███████║██║ ██║ ██║ ╚████╔╝ ███████╗ ██████╔╝███████║ 9 | ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚══════╝ 10 | 11 | This experiment is built using Next.js, Vercel, Algolia, and the Twitter API. 12 | https://github.com/transitive-bullshit/twitter-search 13 | `) 14 | } 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Transitive Bullshit", 3 | "short_name": "Transitive BS", 4 | "icons": [ 5 | { 6 | "src": "/favicon-16x16.png", 7 | "type": "image/png", 8 | "sizes": "16x16" 9 | }, 10 | { 11 | "src": "/favicon-32x32.png", 12 | "type": "image/png", 13 | "sizes": "32x32" 14 | }, 15 | { 16 | "src": "/favicon-96x96.png", 17 | "type": "image/png", 18 | "sizes": "96x96" 19 | }, 20 | { 21 | "src": "/apple-touch-icon.png", 22 | "type": "image/png", 23 | "sizes": "180x180" 24 | }, 25 | { 26 | "src": "/android-chrome-192x192.png", 27 | "sizes": "192x192", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "/android-chrome-512x512.png", 32 | "sizes": "512x512", 33 | "type": "image/png" 34 | } 35 | ], 36 | "theme_color": "#F898B9", 37 | "background_color": "#ffffff", 38 | "display": "standalone" 39 | } 40 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Travis Fischer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/get-tweet-ast/[tweetId].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | import { fetchTweetAst } from 'static-tweets' 3 | import Cors from 'cors' 4 | 5 | const cors = initMiddleware( 6 | Cors({ 7 | methods: ['GET', 'OPTIONS'] 8 | }) 9 | ) 10 | 11 | export default async ( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ): Promise => { 15 | await cors(req, res) 16 | 17 | if (req.method === 'OPTIONS') { 18 | return res.status(200).end() 19 | } 20 | 21 | if (req.method !== 'GET') { 22 | return res.status(405).send({ error: 'method not allowed' }) 23 | } 24 | 25 | const tweetId = req.query.tweetId as string 26 | 27 | if (!tweetId) { 28 | return res 29 | .status(400) 30 | .send({ error: 'missing required parameter "tweetId"' }) 31 | } 32 | 33 | console.log('getTweetAst', tweetId) 34 | const tweetAst = await fetchTweetAst(tweetId) 35 | console.log('tweetAst', tweetId, tweetAst) 36 | 37 | res.status(200).json(tweetAst) 38 | } 39 | 40 | // Helper method to wait for a middleware to execute before continuing 41 | // And to throw an error when an error happens in a middleware 42 | function initMiddleware(middleware) { 43 | return (req, res) => 44 | new Promise((resolve, reject) => { 45 | middleware(req, res, (result) => { 46 | if (result instanceof Error) { 47 | return reject(result) 48 | } 49 | return resolve(result) 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Document, { Html, Head, Main, NextScript } from 'next/document' 3 | 4 | export default class MyDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 28 | 34 | 40 | 41 | 42 | 43 | 44 | 45 |