Move to TanStack Start from Next.js (#184)

This commit is contained in:
Aslam
2024-10-07 16:44:17 +07:00
committed by GitHub
parent 3a89a1c07f
commit 950ebc3dad
514 changed files with 20021 additions and 15508 deletions

33
web/app/lib/utils/env.ts Normal file
View File

@@ -0,0 +1,33 @@
/**
*
* Utility function to get env variables.
*
* @param name env variable name
* @param defaultVaue default value to return if the env variable is not set
* @returns string
*
* @internal
*/
export const getEnvVariable = (
name: string,
defaultVaue: string = "",
): string => {
// Node envs
if (
typeof process !== "undefined" &&
process.env &&
typeof process.env[name] === "string"
) {
return (process.env[name] as string) || defaultVaue
}
if (
typeof import.meta !== "undefined" &&
import.meta.env &&
typeof import.meta.env[name] === "string"
) {
return import.meta.env[name]
}
return defaultVaue
}

View File

@@ -0,0 +1,54 @@
/**
* Resizes the canvas to match the size it is being displayed.
*
* @param canvas the canvas to resize
* @returns `true` if the canvas was resized
*/
export function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {
// Get the size the browser is displaying the canvas in device pixels.
const dpr = window.devicePixelRatio
const { width, height } = canvas.getBoundingClientRect()
const display_width = Math.round(width * dpr)
const display_height = Math.round(height * dpr)
const need_resize =
canvas.width != display_width || canvas.height != display_height
if (need_resize) {
canvas.width = display_width
canvas.height = display_height
}
return need_resize
}
export interface CanvasResizeObserver {
/** Canvas was resized since last check. Set it to `false` to reset. */
resized: boolean
canvas: HTMLCanvasElement
observer: ResizeObserver
}
export function resize(observer: CanvasResizeObserver): boolean {
const resized = resizeCanvasToDisplaySize(observer.canvas)
observer.resized ||= resized
return resized
}
export function resizeObserver(
canvas: HTMLCanvasElement,
): CanvasResizeObserver {
const cro: CanvasResizeObserver = {
resized: false,
canvas: canvas,
observer: null!,
}
cro.observer = new ResizeObserver(resize.bind(null, cro))
resize(cro)
cro.observer.observe(canvas)
return cro
}
export function clear(observer: CanvasResizeObserver): void {
observer.observer.disconnect()
}

View File

@@ -0,0 +1,2 @@
export * from "./canvas"
export * from "./schedule"

View File

@@ -0,0 +1,151 @@
export interface Scheduler<Args extends unknown[]> {
trigger: (...args: Args) => void
clear: () => void
}
/**
* Creates a callback that is debounced and cancellable. The debounced callback is called on **trailing** edge.
*
* @param callback The callback to debounce
* @param wait The duration to debounce in milliseconds
*
* @example
* ```ts
* const debounce = schedule.debounce((message: string) => console.log(message), 250)
* debounce.trigger('Hello!')
* debounce.clear() // clears a timeout in progress
* ```
*/
export function debounce<Args extends unknown[]>(
callback: (...args: Args) => void,
wait?: number,
): Debounce<Args> {
return new Debounce(callback, wait)
}
export class Debounce<Args extends unknown[]> implements Scheduler<Args> {
timeout_id: ReturnType<typeof setTimeout> | undefined
constructor(
public callback: (...args: Args) => void,
public wait?: number,
) {}
trigger(...args: Args): void {
if (this.timeout_id !== undefined) {
this.clear()
}
this.timeout_id = setTimeout(() => {
this.callback(...args)
}, this.wait)
}
clear(): void {
clearTimeout(this.timeout_id)
}
}
/**
* Creates a callback that is throttled and cancellable. The throttled callback is called on **trailing** edge.
*
* @param callback The callback to throttle
* @param wait The duration to throttle
*
* @example
* ```ts
* const throttle = schedule.throttle((val: string) => console.log(val), 250)
* throttle.trigger('my-new-value')
* throttle.clear() // clears a timeout in progress
* ```
*/
export function throttle<Args extends unknown[]>(
callback: (...args: Args) => void,
wait?: number,
): Throttle<Args> {
return new Throttle(callback, wait)
}
export class Throttle<Args extends unknown[]> implements Scheduler<Args> {
is_throttled = false
timeout_id: ReturnType<typeof setTimeout> | undefined
last_args: Args | undefined
constructor(
public callback: (...args: Args) => void,
public wait?: number,
) {}
trigger(...args: Args): void {
this.last_args = args
if (this.is_throttled) {
return
}
this.is_throttled = true
this.timeout_id = setTimeout(() => {
this.callback(...(this.last_args as Args))
this.is_throttled = false
}, this.wait)
}
clear(): void {
clearTimeout(this.timeout_id)
this.is_throttled = false
}
}
/**
* Creates a callback throttled using `window.requestIdleCallback()`. ([MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback))
*
* The throttled callback is called on **trailing** edge.
*
* @param callback The callback to throttle
* @param max_wait maximum wait time in milliseconds until the callback is called
*
* @example
* ```ts
* const idle = schedule.scheduleIdle((val: string) => console.log(val), 250)
* idle.trigger('my-new-value')
* idle.clear() // clears a timeout in progress
* ```
*/
export function scheduleIdle<Args extends unknown[]>(
callback: (...args: Args) => void,
max_wait?: number,
): ScheduleIdle<Args> | Throttle<Args> {
return typeof requestIdleCallback == "function"
? new ScheduleIdle(callback, max_wait)
: new Throttle(callback)
}
export class ScheduleIdle<Args extends unknown[]> implements Scheduler<Args> {
is_deferred = false
request_id: ReturnType<typeof requestIdleCallback> | undefined
last_args: Args | undefined
constructor(
public callback: (...args: Args) => void,
public max_wait?: number,
) {}
trigger(...args: Args): void {
this.last_args = args
if (this.is_deferred) {
return
}
this.is_deferred = true
this.request_id = requestIdleCallback(
() => {
this.callback(...(this.last_args as Args))
this.is_deferred = false
},
{ timeout: this.max_wait },
)
}
clear(): void {
if (this.request_id != undefined) {
cancelIdleCallback(this.request_id)
}
this.is_deferred = false
}
}

View File

@@ -0,0 +1,81 @@
import * as React from "react"
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
export const searchSafeRegExp = (inputValue: string) => {
const escapedChars = inputValue.split("").map(escapeRegExp)
return new RegExp(escapedChars.join(".*"), "i")
}
export function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
return shuffled
}
export const isClient = () => typeof window !== "undefined"
export const isServer = () => !isClient()
const inputs = ["input", "select", "button", "textarea"] // detect if node is a text input element
export function isTextInput(element: Element): boolean {
return !!(
element &&
element.tagName &&
(inputs.indexOf(element.tagName.toLowerCase()) !== -1 ||
element.attributes.getNamedItem("role")?.value === "textbox" ||
element.attributes.getNamedItem("contenteditable")?.value === "true")
)
}
export type HTMLAttributes = React.HTMLAttributes<HTMLElement> & {
[key: string]: any
}
export type HTMLLikeElement = {
tag: keyof JSX.IntrinsicElements
attributes?: HTMLAttributes
children?: (HTMLLikeElement | string)[]
}
export const renderHTMLLikeElement = (
element: HTMLLikeElement | string,
): React.ReactNode => {
if (typeof element === "string") {
return element
}
const { tag, attributes = {}, children = [] } = element
return React.createElement(
tag,
attributes,
...children.map((child) => renderHTMLLikeElement(child)),
)
}
export function calendarFormatDate(date: Date): string {
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
}
export * from "./force-graph"
export * from "./keyboard"
export * from "./env"
export * from "./slug"
export * from "./url"

View File

@@ -0,0 +1,67 @@
import { isServer } from "."
interface ShortcutKeyResult {
symbol: string
readable: string
}
export function getShortcutKey(key: string): ShortcutKeyResult {
const lowercaseKey = key.toLowerCase()
if (lowercaseKey === "mod") {
return isMac()
? { symbol: "⌘", readable: "Command" }
: { symbol: "Ctrl", readable: "Control" }
} else if (lowercaseKey === "alt") {
return isMac()
? { symbol: "⌥", readable: "Option" }
: { symbol: "Alt", readable: "Alt" }
} else if (lowercaseKey === "shift") {
return isMac()
? { symbol: "⇧", readable: "Shift" }
: { symbol: "Shift", readable: "Shift" }
} else {
return { symbol: key.toUpperCase(), readable: key }
}
}
export function getShortcutKeys(keys: string[]): ShortcutKeyResult[] {
return keys.map((key) => getShortcutKey(key))
}
export function isModKey(
event: KeyboardEvent | MouseEvent | React.KeyboardEvent,
) {
return isMac() ? event.metaKey : event.ctrlKey
}
export function isMac(): boolean {
if (isServer()) {
return false
}
return window.navigator.platform === "MacIntel"
}
export function isWindows(): boolean {
if (isServer()) {
return false
}
return window.navigator.platform === "Win32"
}
let supportsPassive = false
try {
const opts = Object.defineProperty({}, "passive", {
get() {
supportsPassive = true
},
})
// @ts-expect-error ts-migrate(2769) testPassive is not a real event
window.addEventListener("testPassive", null, opts)
// @ts-expect-error ts-migrate(2769) testPassive is not a real event
window.removeEventListener("testPassive", null, opts)
} catch (e) {
// No-op
}
export const supportsPassiveListener = supportsPassive

View File

@@ -0,0 +1,43 @@
/*
* This file contains custom schema definitions for Zod.
*/
import { z } from "zod"
export const urlSchema = z
.string()
.min(1, { message: "URL can't be empty" })
.refine(
(value) => {
const domainRegex =
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
const isValidDomain = (domain: string) => {
try {
const url = new URL(`http://${domain}`)
return domainRegex.test(url.hostname)
} catch {
return false
}
}
if (isValidDomain(value)) {
return true
}
try {
const url = new URL(value)
if (!url.protocol.match(/^https?:$/)) {
return false
}
return isValidDomain(url.hostname)
} catch {
return false
}
},
{
message: "Please enter a valid URL",
},
)

33
web/app/lib/utils/seo.ts Normal file
View File

@@ -0,0 +1,33 @@
export const seo = ({
title,
description,
keywords,
image,
}: {
title: string
description?: string
image?: string
keywords?: string
}) => {
const tags = [
{ title },
{ name: "description", content: description },
{ name: "keywords", content: keywords },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
{ name: "twitter:creator", content: "@tannerlinsley" },
{ name: "twitter:site", content: "@tannerlinsley" },
{ name: "og:type", content: "website" },
{ name: "og:title", content: title },
{ name: "og:description", content: description },
...(image
? [
{ name: "twitter:image", content: image },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "og:image", content: image },
]
: []),
]
return tags
}

22
web/app/lib/utils/slug.ts Normal file
View File

@@ -0,0 +1,22 @@
import slugify from "slugify"
export function generateUniqueSlug(
title: string,
maxLength: number = 60,
): string {
const baseSlug = slugify(title, {
lower: true,
strict: true,
})
// Web Crypto API
const randomValues = new Uint8Array(4)
crypto.getRandomValues(randomValues)
const randomSuffix = Array.from(randomValues)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
const truncatedSlug = baseSlug.slice(0, Math.min(maxLength, 75) - 9)
return `${truncatedSlug}-${randomSuffix}`
}

25
web/app/lib/utils/url.ts Normal file
View File

@@ -0,0 +1,25 @@
export function isValidUrl(string: string): boolean {
try {
new URL(string)
return true
} catch (_) {
return false
}
}
export function isUrl(text: string): boolean {
const pattern: RegExp =
/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/
return pattern.test(text)
}
export function ensureUrlProtocol(
url: string,
defaultProtocol: string = "https://",
): string {
if (url.match(/^[a-zA-Z]+:\/\//)) {
return url
}
return `${defaultProtocol}${url.startsWith("//") ? url.slice(2) : url}`
}