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"