1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 04:45:22 -04:00

react-twitter-embed ➡️ react-tweet

This commit is contained in:
Jake Jarvis 2023-09-08 11:47:38 -04:00
parent 61660d9d56
commit 1d8c2eab99
Signed by: jake
GPG Key ID: 2B0C9CF251E69A39
25 changed files with 789 additions and 719 deletions

View File

@ -1,7 +1,7 @@
import Code from "../Code";
import CopyButton from "../CopyButton";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Block = styled("div", {
position: "relative",
@ -87,7 +87,7 @@ const CornerCopyButton = styled(CopyButton, {
transition: `background ${theme.transitions.fade}, border ${theme.transitions.fade}`,
});
export type CodeBlockProps = ComponentProps<typeof Code> & {
export type CodeBlockProps = ComponentPropsWithoutRef<typeof Code> & {
highlight?: boolean;
withCopyButton?: boolean;
};

View File

@ -2,7 +2,7 @@ import Giscus from "@giscus/react";
import useTheme from "../../hooks/useTheme";
import { styled, theme } from "../../lib/styles/stitches.config";
import { giscusConfig } from "../../lib/config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
import type { GiscusProps } from "@giscus/react";
const Wrapper = styled("div", {
@ -12,7 +12,7 @@ const Wrapper = styled("div", {
minHeight: "360px",
});
export type CommentsProps = ComponentProps<typeof Wrapper> & {
export type CommentsProps = ComponentPropsWithoutRef<typeof Wrapper> & {
title: string;
};

View File

@ -3,7 +3,7 @@ import innerText from "react-innertext";
import copy from "copy-to-clipboard";
import { ClipboardOcticon, CheckOcticon } from "../Icons";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ReactNode, Ref, ComponentPropsWithoutRef, MouseEventHandler } from "react";
import type { ReactNode, Ref, ComponentPropsWithoutRef, ElementRef, MouseEventHandler } from "react";
const Button = styled("button", {
lineHeight: 1,
@ -37,10 +37,10 @@ export type CopyButtonProps = ComponentPropsWithoutRef<typeof Button> & {
timeout?: number;
};
const CopyButton = ({ source, timeout = 2000, ...rest }: CopyButtonProps, ref: Ref<HTMLButtonElement>) => {
const CopyButton = ({ source, timeout = 2000, ...rest }: CopyButtonProps, ref: Ref<ElementRef<typeof Button>>) => {
const [copied, setCopied] = useState(false);
const handleCopy: MouseEventHandler<HTMLButtonElement> = (e) => {
const handleCopy: MouseEventHandler<ElementRef<typeof Button>> = (e) => {
// prevent unintentional double-clicks by unfocusing button
e.currentTarget.blur();

View File

@ -2,7 +2,7 @@ import Link from "../Link";
import { HeartIcon, NextjsLogo } from "../Icons";
import { styled, theme, keyframes } from "../../lib/styles/stitches.config";
import * as config from "../../lib/config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Wrapper = styled("footer", {
width: "100%",
@ -78,7 +78,7 @@ const Heart = styled("span", {
},
});
export type FooterProps = ComponentProps<typeof Wrapper>;
export type FooterProps = ComponentPropsWithoutRef<typeof Wrapper>;
const Footer = ({ ...rest }: FooterProps) => {
return (

View File

@ -1,7 +1,7 @@
import Selfie from "../Selfie";
import Menu from "../Menu";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Wrapper = styled("header", {
width: "100%",
@ -40,7 +40,7 @@ const ResponsiveMenu = styled(Menu, {
},
});
export type HeaderProps = ComponentProps<typeof Wrapper>;
export type HeaderProps = ComponentPropsWithoutRef<typeof Wrapper>;
const Header = ({ ...rest }: HeaderProps) => {
return (

View File

@ -1,7 +1,7 @@
import innerText from "react-innertext";
import HeadingAnchor from "../HeadingAnchor";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Anchor = styled(HeadingAnchor, {
margin: "0 0.4em",
@ -52,7 +52,7 @@ const H = styled("h1", {
},
});
export type HeadingProps = ComponentProps<typeof H> & {
export type HeadingProps = ComponentPropsWithoutRef<typeof H> & {
level: 1 | 2 | 3 | 4 | 5 | 6;
divider?: boolean;
};

View File

@ -1,7 +1,7 @@
import Link from "../Link";
import { LinkIcon } from "../Icons";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const AnchorLink = styled(Link, {
lineHeight: 1,
@ -12,7 +12,7 @@ const Icon = styled(LinkIcon, {
height: "0.8em",
});
export type HeadingAnchorProps = Omit<ComponentProps<typeof AnchorLink>, "href"> & {
export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof AnchorLink>, "href"> & {
id: string;
title: string;
};

View File

@ -1,5 +1,5 @@
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const RoundedIFrame = styled("iframe", {
width: "100%",
@ -9,7 +9,7 @@ const RoundedIFrame = styled("iframe", {
borderRadius: theme.radii.corner,
});
export type IFrameProps = ComponentProps<typeof RoundedIFrame> & {
export type IFrameProps = ComponentPropsWithoutRef<typeof RoundedIFrame> & {
src: string;
height: number;
width?: number; // defaults to 100%

View File

@ -1,7 +1,7 @@
import NextImage from "next/image";
import Link from "../Link";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
import type { ImageProps as NextImageProps, StaticImageData } from "next/image";
const DEFAULT_QUALITY = 60;
@ -22,7 +22,7 @@ const StyledImage = styled(NextImage, {
borderRadius: theme.radii.corner,
});
export type ImageProps = ComponentProps<typeof StyledImage> & {
export type ImageProps = ComponentPropsWithoutRef<typeof StyledImage> & {
href?: string; // optionally wrap image in a link
inline?: boolean; // don't wrap everything in a `<div>` block
};

View File

@ -4,7 +4,7 @@ import Footer from "../Footer";
import { SkipToContentLink, SkipToContentTarget } from "../SkipToContent";
import useTheme from "../../hooks/useTheme";
import { styled, theme, darkTheme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Flex = styled("div", {
display: "flex",
@ -34,7 +34,7 @@ const FlexedFooter = styled(Footer, {
flex: 1,
});
export type LayoutProps = ComponentProps<typeof Flex> & {
export type LayoutProps = ComponentPropsWithoutRef<typeof Flex> & {
container?: boolean; // pass false to disable default `<main>` container styles with padding, etc.
};

View File

@ -1,7 +1,7 @@
import NextLink from "next/link";
import objStr from "obj-str";
import { styled, theme, stitchesConfig } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const StyledLink = styled(NextLink, {
color: theme.colors.link,
@ -34,7 +34,7 @@ const StyledLink = styled(NextLink, {
},
});
export type LinkProps = ComponentProps<typeof StyledLink> & {
export type LinkProps = ComponentPropsWithoutRef<typeof StyledLink> & {
openInNewTab?: boolean;
};

View File

@ -1,5 +1,5 @@
import { styled, theme, keyframes } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Wrapper = styled("div", {
display: "inline-block",
@ -20,7 +20,7 @@ const Box = styled("div", {
backgroundColor: theme.colors.mediumLight,
});
export type LoadingProps = ComponentProps<typeof Wrapper> & {
export type LoadingProps = ComponentPropsWithoutRef<typeof Wrapper> & {
width: number; // of entire container, in pixels
boxes?: number; // total number of boxes (default: 3)
timing?: number; // staggered timing between each box's pulse, in seconds (default: 0.1s)

View File

@ -3,7 +3,7 @@ import MenuItem from "../MenuItem";
import ThemeToggle from "../ThemeToggle";
import { styled } from "../../lib/styles/stitches.config";
import { menuItems } from "../../lib/config/menu";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Wrapper = styled("ul", {
display: "inline-flex",
@ -38,7 +38,7 @@ const Item = styled("li", {
},
});
export type MenuProps = ComponentProps<typeof Wrapper>;
export type MenuProps = ComponentPropsWithoutRef<typeof Wrapper>;
const Menu = ({ ...rest }: MenuProps) => {
const router = useRouter();

View File

@ -1,6 +1,6 @@
import Link from "../Link";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
import type { NoteFrontMatter } from "../../types";
const Title = styled("h1", {
@ -22,7 +22,8 @@ const TitleLink = styled(Link, {
color: theme.colors.text,
});
export type NoteTitleProps = Pick<NoteFrontMatter, "slug" | "title" | "htmlTitle"> & ComponentProps<typeof Title>;
export type NoteTitleProps = Pick<NoteFrontMatter, "slug" | "title" | "htmlTitle"> &
ComponentPropsWithoutRef<typeof Title>;
const NoteTitle = ({ slug, title, htmlTitle, ...rest }: NoteTitleProps) => {
return (

View File

@ -1,7 +1,7 @@
import Link from "../Link";
import { OctocatOcticon } from "../Icons";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const GitHubLink = styled(Link, {
margin: "0 0.4em",
@ -19,7 +19,7 @@ const Octocat = styled(OctocatOcticon, {
fill: "currentColor",
});
export type OctocatLinkProps = Omit<ComponentProps<typeof GitHubLink>, "href"> & {
export type OctocatLinkProps = Omit<ComponentPropsWithoutRef<typeof GitHubLink>, "href"> & {
repo: string;
};

View File

@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import Link from "../Link";
import { styled, theme } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const Title = styled("h1", {
marginTop: 0,
@ -19,7 +19,7 @@ const TitleLink = styled(Link, {
color: theme.colors.text,
});
export type PageTitleProps = ComponentProps<typeof Title>;
export type PageTitleProps = ComponentPropsWithoutRef<typeof Title>;
const PageTitle = ({ children, ...rest }: PageTitleProps) => {
const router = useRouter();

View File

@ -2,7 +2,7 @@ import Link from "../Link";
import Image from "../Image";
import { styled, theme } from "../../lib/styles/stitches.config";
import { authorName } from "../../lib/config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
import selfieJpg from "../../public/static/images/selfie.jpg";
@ -47,7 +47,7 @@ const Name = styled("span", {
},
});
export type SelfieProps = Omit<ComponentProps<typeof Link>, "href">;
export type SelfieProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href">;
const Selfie = ({ ...rest }: SelfieProps) => {
return (

View File

@ -1,5 +1,5 @@
import { keyframes, styled } from "../../lib/styles/stitches.config";
import type { ComponentProps } from "react";
import type { ComponentPropsWithoutRef } from "react";
const BlackBox = styled("div", {
width: "100%",
@ -31,7 +31,7 @@ const Underscore = styled("span", {
animation: `${keyframes({ "40%": { opacity: 0 } })} 1s step-end infinite`,
});
export type TerminalProps = ComponentProps<typeof BlackBox>;
export type TerminalProps = ComponentPropsWithoutRef<typeof BlackBox>;
// a DOS-style terminal box with dynamic text
const Terminal = ({ children: message, ...rest }: TerminalProps) => {

View File

@ -1,40 +1,47 @@
import { useState } from "react";
import { TwitterTweetEmbed } from "react-twitter-embed";
import useUpdateEffect from "../../hooks/useUpdateEffect";
import { useEffect, useRef } from "react";
import Image from "next/image";
import { Tweet } from "react-tweet";
import useTheme from "../../hooks/useTheme";
import { styled } from "../../lib/styles/stitches.config";
import type { ComponentPropsWithoutRef, ElementRef } from "react";
const Wrapper = styled("div", {
// reserve a moderate amount of space for the widget, it takes a while to load...
minHeight: "300px",
minHeight: "300px", // help with layout shift
"& .react-tweet-theme": {
"--tweet-container-margin": "1.5rem auto",
},
});
export type TweetEmbedProps = {
id: string;
options?: Record<string, unknown>;
export type TweetEmbedProps = ComponentPropsWithoutRef<typeof Tweet> & {
className?: string;
};
const TweetEmbed = ({ id, options, className }: TweetEmbedProps) => {
const TweetEmbed = ({ id, className, ...rest }: TweetEmbedProps) => {
const containerRef = useRef<ElementRef<typeof Wrapper>>(null);
const { activeTheme } = useTheme();
const [widgetTheme, setWidgetTheme] = useState(activeTheme);
useUpdateEffect(() => {
setWidgetTheme(activeTheme === "dark" ? activeTheme : "light");
useEffect(() => {
if (containerRef.current) {
// setting 'data-theme' attribute of parent div changes the tweet's theme (no re-render necessary)
containerRef.current.dataset.theme = activeTheme;
}
}, [activeTheme]);
return (
<Wrapper className={className}>
<TwitterTweetEmbed
tweetId={id}
options={{
dnt: true,
align: "center",
theme: widgetTheme,
...options,
<Wrapper ref={containerRef} className={className}>
<Tweet
key={`tweet-${id}`}
id={id}
apiUrl={`/api/tweet/?id=${id}`} // edge function at pages/api/tweet.ts
components={{
// https://react-tweet.vercel.app/twitter-theme/api-reference#custom-tweet-components
// eslint-disable-next-line jsx-a11y/alt-text
AvatarImg: (props) => <Image {...props} />,
// eslint-disable-next-line jsx-a11y/alt-text
MediaImg: (props) => <Image {...props} fill />,
}}
// changing this key forces the iframe URL to reformulate itself and update the theme:
key={`tweet-${id}-${widgetTheme}`}
{...rest}
/>
</Wrapper>
);

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import RFB from "@novnc/novnc/core/rfb";
import Terminal from "../Terminal";
import { styled } from "../../lib/styles/stitches.config";
import type { Ref, ComponentPropsWithoutRef } from "react";
import type { Ref, ComponentPropsWithoutRef, ElementRef } from "react";
const Display = styled(
"div",
@ -50,7 +50,7 @@ const VNC = ({ server, style, ...rest }: VNCProps, ref: Ref<Partial<RFB>>) => {
// the actual connection and virtual screen (injected by noVNC when it's ready)
const rfbRef = useRef<RFB | null>(null);
const screenRef = useRef<HTMLDivElement>(null);
const displayRef = useRef<ElementRef<typeof Display>>(null);
// ends the session forcefully
const disconnect = useCallback(() => {
@ -121,7 +121,7 @@ const VNC = ({ server, style, ...rest }: VNCProps, ref: Ref<Partial<RFB>>) => {
setLoaded(true);
// https://github.com/novnc/noVNC/blob/master/docs/API.md
rfbRef.current = new RFB(screenRef.current as Element, server, {
rfbRef.current = new RFB(displayRef.current as Element, server, {
wsProtocols: ["binary", "base64"],
});
@ -164,7 +164,7 @@ const VNC = ({ server, style, ...rest }: VNCProps, ref: Ref<Partial<RFB>>) => {
{/* the VNC canvas is hidden until we've successfully connected to the socket */}
<Display
ref={screenRef}
ref={displayRef}
style={{
display: !connected ? "none" : undefined,
...style,

View File

@ -11,7 +11,10 @@ const nextConfig = {
reactStrictMode: true,
trailingSlash: true,
productionBrowserSourceMaps: true,
transpilePackages: ["@novnc/novnc"],
transpilePackages: [
"@novnc/novnc",
"react-tweet", // https://react-tweet.vercel.app/next#troubleshooting
],
env: {
BASE_URL:
// start with production check on Vercel, then see if this is a deploy preview, then fallback to local dev server.
@ -27,6 +30,10 @@ const nextConfig = {
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
formats: ["image/avif", "image/webp"],
remotePatterns: [
{ protocol: "https", hostname: "pbs.twimg.com" },
{ protocol: "https", hostname: "abs.twimg.com" },
],
},
experimental: {
legacyBrowsers: false,

View File

@ -20,13 +20,13 @@
"dependencies": {
"@giscus/react": "^2.3.0",
"@hcaptcha/react-hcaptcha": "^1.8.1",
"@novnc/novnc": "github:novnc/novnc#ca6527c1bf7131adccfdcc5028964a1e67f9018c",
"@novnc/novnc": "1.4.0-ge81602d",
"@octokit/graphql": "^7.0.1",
"@octokit/graphql-schema": "^14.27.2",
"@octokit/graphql-schema": "^14.27.3",
"@primer/octicons": "^19.7.0",
"@prisma/client": "^5.2.0",
"@react-spring/web": "^9.7.3",
"@stitches/react": "^1.3.1-1",
"@stitches/react": "1.3.1-1",
"@vercel/postgres": "^0.4.1",
"comma-number": "^2.1.0",
"copy-to-clipboard": "^3.3.3",
@ -35,7 +35,7 @@
"fathom-client": "^3.5.0",
"feather-icons": "^4.29.1",
"feed": "^4.2.2",
"formik": "^2.4.3",
"formik": "^2.4.4",
"gray-matter": "^4.0.3",
"next": "13.4.19",
"next-mdx-remote": "^4.4.1",
@ -55,7 +55,7 @@
"react-is": "18.2.0",
"react-player": "~2.10.1",
"react-textarea-autosize": "^8.5.3",
"react-twitter-embed": "^4.0.4",
"react-tweet": "^3.1.1",
"rehype-prism-plus": "^1.6.3",
"rehype-sanitize": "^5.0.1",
"rehype-slug": "^5.1.0",
@ -66,17 +66,17 @@
"remark-smartypants": "^2.0.0",
"remark-unwrap-images": "^3.0.1",
"remove-markdown": "^0.5.0",
"simple-icons": "^9.12.0",
"simple-icons": "^9.13.0",
"sitemap": "^7.1.1",
"stitches-normalize": "^2.0.0",
"swr": "^2.2.2",
"unified": "^10.1.2"
},
"devDependencies": {
"@jakejarvis/eslint-config": "github:jakejarvis/eslint-config",
"@jakejarvis/eslint-config": "^3.1.0",
"@svgr/webpack": "^8.1.0",
"@types/comma-number": "^2.1.0",
"@types/node": "^18.17.12",
"@types/node": "^18.17.14",
"@types/novnc__novnc": "^1.3.0",
"@types/prop-types": "^15.7.5",
"@types/react": "^18.2.21",
@ -84,8 +84,8 @@
"@types/react-is": "^18.2.1",
"@types/remove-markdown": "^0.3.1",
"@types/uglify-js": "^3.17.2",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"cross-env": "^7.0.3",
"eslint": "~8.48.0",
"eslint-config-next": "13.4.19",
@ -117,9 +117,9 @@
"eslint"
]
},
"packageManager": "pnpm@8.7.1",
"packageManager": "pnpm@8.7.4",
"volta": {
"node": "18.17.1",
"pnpm": "8.7.1"
"pnpm": "8.7.4"
}
}

36
pages/api/tweet.ts Normal file
View File

@ -0,0 +1,36 @@
import { NextResponse } from "next/server";
import { getTweet } from "react-tweet/api";
import type { NextRequest } from "next/server";
export const config = {
runtime: "edge",
};
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextRequest) => {
const tweetId = req.nextUrl.searchParams.get("id");
if (typeof tweetId !== "string") {
return NextResponse.json({ error: "Bad request." }, { status: 400 });
}
// https://react-tweet.vercel.app/twitter-theme/api-reference
try {
const tweet = await getTweet(tweetId);
return NextResponse.json(
{ data: tweet ?? null },
{
status: tweet ? 200 : 404,
headers: {
// cache on edge for 12 hours
"Cache-Control": "public, max-age=0, s-maxage=43200, stale-while-revalidate",
},
}
);
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any
) {
return NextResponse.json({ error: error.message ?? "Bad request." }, { status: 400 });
}
};

View File

@ -6,7 +6,7 @@ import { NextSeo } from "next-seo";
import Layout from "../components/Layout";
import Terminal from "../components/Terminal";
import { styled } from "../lib/styles/stitches.config";
import type { ReactElement, ComponentProps } from "react";
import type { ReactElement, ComponentPropsWithoutRef, ElementRef } from "react";
// obviously, an interactive VNC display will not work even a little bit server-side
const VNC = dynamic(() => import("../components/VNC"), { ssr: false });
@ -25,8 +25,8 @@ const Wallpaper = styled("main", {
backgroundPosition: "center",
});
const RandomWallpaper = ({ ...rest }: ComponentProps<typeof Wallpaper>) => {
const wallpaperRef = useRef<HTMLDivElement>(null);
const RandomWallpaper = ({ ...rest }: ComponentPropsWithoutRef<typeof Wallpaper>) => {
const wallpaperRef = useRef<ElementRef<typeof Wallpaper>>(null);
// set a random retro Windows ME desktop tile for the entire content area
useEffect(() => {

1299
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff