This commit is contained in:
Jared Palmer 2023-05-19 12:33:56 -04:00
commit a04776256d
56 changed files with 6808 additions and 0 deletions

View file

@ -0,0 +1,10 @@
import { isAppleDevice } from "@/lib/tinykeys";
/**
* Uses the Meta key on macOS, and the Ctrl key on other platforms.
*/
export const isModifierPressed = (
event: KeyboardEvent | React.KeyboardEvent
): boolean => {
return isAppleDevice() ? event.metaKey : event.ctrlKey;
};

View file

@ -0,0 +1,21 @@
import type { RefObject } from "react";
import { useRef } from "react";
import { isModifierPressed } from "./is-modifier-pressed";
export function useCmdEnterSubmit(): {
formRef: RefObject<HTMLFormElement>;
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
} {
const formRef = useRef<HTMLFormElement>(null);
const handleKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement>
): void => {
// Capture `⌘`|Ctrl + Enter
if (isModifierPressed(event) && event.key === "Enter") {
formRef.current?.requestSubmit();
}
};
return { formRef, onKeyDown: handleKeyDown };
}

View file

@ -0,0 +1,32 @@
'use client';
import { useState } from 'react';
export interface useCopyToClipboardProps {
timeout?: number;
}
export function useCopyToClipboard({
timeout = 2000,
}: useCopyToClipboardProps) {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const copyToClipboard = (value: string) => {
if (
typeof window === 'undefined' ||
!navigator.clipboard ||
!navigator.clipboard.writeText
) {
return;
}
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, timeout);
});
};
return { isCopied, copyToClipboard };
}

View file

@ -0,0 +1,43 @@
import { useEffect, useRef } from "react";
const scrollToEnd = () => {
window.scrollTo({
top: document.body.scrollHeight,
left: 0,
});
};
export function useFollowScroll(shouldFollow: boolean) {
const shouldFollowRef = useRef(shouldFollow);
const scrollRef = useRef(false);
const triggeredBySelfRef = useRef(false);
useEffect(() => {
const handleScroll = () => {
if (triggeredBySelfRef.current) {
triggeredBySelfRef.current = false;
return;
}
scrollRef.current = true;
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
if (!scrollRef.current && shouldFollowRef.current) {
setTimeout(() => {
triggeredBySelfRef.current = true;
scrollToEnd();
});
}
});
useEffect(() => {
shouldFollowRef.current = shouldFollow;
if (!shouldFollow) {
// Reset scrollRef
scrollRef.current = false;
}
}, [shouldFollow]);
}

View file

@ -0,0 +1,43 @@
import { RefObject, useEffect, useState } from "react";
interface Args extends IntersectionObserverInit {
freezeOnceVisible?: boolean;
}
function useIntersectionObserver(
elementRef: RefObject<Element>,
{
threshold = 0,
root = null,
rootMargin = "0%",
freezeOnceVisible = false,
}: Args
): IntersectionObserverEntry | undefined {
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const frozen = entry?.isIntersecting && freezeOnceVisible;
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
setEntry(entry);
};
useEffect(() => {
const node = elementRef?.current; // DOM Ref
const hasIOSupport = !!window.IntersectionObserver;
if (!hasIOSupport || frozen || !node) return;
const observerParams = { threshold, root, rootMargin };
const observer = new IntersectionObserver(updateEntry, observerParams);
observer.observe(node);
return () => observer.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [threshold, root, rootMargin, frozen]);
return entry;
}
export default useIntersectionObserver;

View file

@ -0,0 +1,26 @@
import { useEffect, useState } from "react";
const useLocalStorage = <T>(
key: string,
initialValue: T
): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState(initialValue);
useEffect(() => {
// Retrieve from localStorage
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
}
}, [key]);
const setValue = (value: T) => {
// Save state
setStoredValue(value);
// Save to localStorage
window.localStorage.setItem(key, JSON.stringify(value));
};
return [storedValue, setValue];
};
export default useLocalStorage;

16
lib/hooks/use-scroll.ts Normal file
View file

@ -0,0 +1,16 @@
import { useCallback, useEffect, useState } from "react";
export default function useScroll(threshold: number) {
const [scrolled, setScrolled] = useState(false);
const onScroll = useCallback(() => {
setScrolled(window.pageYOffset > threshold);
}, [threshold]);
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [onScroll]);
return scrolled;
}

View file

@ -0,0 +1,39 @@
"use client";
import { useEffect, useState } from "react";
export default function useWindowSize() {
const [windowSize, setWindowSize] = useState<{
width: number | undefined;
height: number | undefined;
}>({
width: undefined,
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return {
windowSize,
isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768,
isDesktop:
typeof windowSize?.width === "number" && windowSize?.width >= 768,
};
}