Snippets

Pieces of code that i keep coming back to / find super cool :)


Conditional Link

Conditionally render Next/Link or a tag based on the link

Link.tsx
import React, { DetailedHTMLProps, AnchorHTMLAttributes, Ref } from 'react';
import NextLink from 'next/link';

type LinkProps = DetailedHTMLProps<
  AnchorHTMLAttributes<HTMLAnchorElement>,
  HTMLAnchorElement
> & {
  ref?: Ref<HTMLAnchorElement>;
};

const Link = ({ href, className, children, ...rest }: LinkProps) => {
  const isInternalLink = href && (href.startsWith('/') || href.startsWith('#'));

  if (isInternalLink) {
    return (
      <NextLink
        href={href}
        className={`text-blue-500 hover:text-blue-600 focus:text-blue-600 active:text-blue-600 underline transition-colors duration-150 ease-in ${className}`}
        {...rest}
      >
        {children}
      </NextLink>
    );
  }

  return (
    <a
      className={`text-blue-500 hover:text-blue-600 focus:text-blue-600 active:text-blue-600 underline transition-colors duration-150 ease-in ${className}`}
      target='_blank'
      rel='noopener noreferrer'
      href={href}
      {...rest}
    >
      {children}
    </a>
  );
};

export default Link;


Google Sheets as DB

Use Google Sheets as a database

import { google } from "googleapis";
const auth = new google.auth.GoogleAuth({
  credentials: {
    client_email: process.env.GOOGLE_CLIENT_EMAIL,
    client_id: process.env.CLIENT_ID,
    private_key: process.env.GOOGLE_SERVICE_PRIVATE_KEY.replace(/\\n/g, "\n"),
  },
  scopes: [
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/drive.file",
    "https://www.googleapis.com/auth/spreadsheets",
  ],
});

const sheets = google.sheets({
  auth,
  version: "v4",
});

const response = await sheets.spreadsheets.values.append({
  spreadsheetId: process.env.SPREADSHEET_ID,
  range: "Sheet1!A2:C",
  valueInputOption: "USER_ENTERED",
  requestBody: {
    values: [Object.values(fields)],
  },
});

SEO Component

Improve SEO using this component

Seo.tsx
import Head from 'next/head';
import { useRouter } from 'next/router';

const defaultMeta = {
  title: 'Vishnu Vinod - Software Engineer',
  siteName: 'vishnuu.com',
  description: 'Portfolio Website. ',
  url: 'https://vishnuu.com',
  image: 'https://vishnuu.com/me.png',
  type: 'website',
  robots: 'follow, index',
};

interface SeoProps extends Partial<typeof defaultMeta> {
  date?: string;
  canonical?: string;
}

export default function Seo(props: SeoProps) {
  const router = useRouter();
  const meta = {
    ...defaultMeta,
    ...props,
  };

  return (
    <Head>
      <title>{meta.title}</title>
      <meta name='robots' content={meta.robots} />
      <meta content={meta.description} name='description' />
      <meta property='og:url' content={`${meta.url}${router.asPath}`} />
      <link
        rel='canonical'
        href={meta.canonical ? meta.canonical : `${meta.url}${router.asPath}`}
      />
      {/* Open Graph */}
      <meta property='og:type' content={meta.type} />
      <meta property='og:site_name' content={meta.siteName} />
      <meta property='og:description' content={meta.description} />
      <meta property='og:title' content={meta.title} />
      <meta name='image' property='og:image' content={meta.image} />
      {/* Twitter */}
      <meta name='twitter:card' content='summary_large_image' />
      {/* <meta name="twitter:site" content="@vishnuvinod" /> */}
      <meta name='twitter:title' content={meta.title} />
      <meta name='twitter:description' content={meta.description} />
      <meta name='twitter:image' content={meta.image} />
      <meta property='twitter:domain' content={meta.siteName} />
      <meta property='twitter:url' content={meta.url} />

      {meta.date && (
        <>
          <meta property='article:published_time' content={meta.date} />
          <meta
            name='publish_date'
            property='og:publish_date'
            content={meta.date}
          />
          <meta
            name='author'
            property='article:author'
            content='Vishnu Vinod - Software Engineer'
          />
        </>
      )}

      <link rel='icon' href='/favicon.svg' />


      <meta name='msapplication-TileColor' content='#ffffff' />
      <meta
        name='msapplication-TileImage'
        content='/favicon/ms-icon-144x144.png'
      />
      <meta name='theme-color' content='#ffffff' />
    </Head>
  );
}


Animate video on Scroll

Animate bunch of images generated from video on scroll using framer-motion

credits: Asutin Malerba

App.tsx
import { Box, ChakraProvider } from "@chakra-ui/react";
import { useViewportScroll } from "framer-motion";
import ImageSequence from "./ImageSequence";
import range from "lodash.range";

const images = Array(55).map((i) => {
  const paddedIndex = i.toString().padStart(4, "0");
  return `https://www.apple.com/105/media/us/airpods-3rd-generation/2021/3c0b27aa-a5fe-4365-a9ae-83c28d10fa21/anim/battery/large/${paddedIndex}.jpg`;
});

function App() {
  const { scrollYProgress } = useViewportScroll();
  return (
    <Box h="1000vh">
      <Box pos="sticky" top={0} h="100vh">
        <ImageSequence
          progress={scrollYProgress}
          images={images}
          width={1464}
          height={824}
          style={{
            position: "absolute",
            width: "100%",
            height: "100%",
            objectFit: "contain",
          }}
        />
      </Box>
    </Box>
  );
}

ImageSequence.tsx
import { CSSProperties } from "react";
import { clamp, MotionValue, useMotionValueEvent } from "framer-motion";
import React, { useEffect, useRef } from "react";

interface ImageSequenceProps {
  progress: MotionValue<number>;
  images: string[];
  height: number;
  width: number;
  style?: CSSProperties;
  className?: string;
}

const ImageSequence = ({
  progress,
  images,
  width,
  height,
  style,
  className,
}: ImageSequenceProps) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const imgRefs = useRef<HTMLImageElement[]>([]);
  const contextRef = useRef<CanvasRenderingContext2D | null | undefined>(null);
  const frameRef = useRef<number>();

  const setFrame = (rawFrame: number) => {
    const frame = clamp(0, images.length - 1, Math.floor(rawFrame));
    if (frameRef.current !== frame && contextRef.current) {
      frameRef.current = frame;
      contextRef.current.drawImage(imgRefs.current[frame], 0, 0);
    }
  };

  useMotionValueEvent(progress, "change", (val) => {
    const frame = clamp(0, images.length - 1, Math.floor(val * images.length));
    setFrame(frame);
  });

  useEffect(() => {
    contextRef.current = canvasRef.current?.getContext("2d");
    imgRefs.current = images.map((src) => {
      const img = new Image();
      img.src = src;
      return img;
    });
    imgRefs.current[0].onload = () => {
      setFrame(0);
    };
  }, []);

  return (
    <canvas
      ref={canvasRef}
      className={className}
      style={style}
      width={width}
      height={height}
    />
  );
};

export default ImageSequence;

Now it can be called using:

<ImageSequence
  images={images}
  width={1464}
  height={824}
  progress={scrollYProgress}
/>

To convert a video to a sequence of images you can use ffmpeg:

ffmpeg -¡ "airpods.mp4" "frames/%04d.jpg"

See all posts