import { useMatches } from "@remix-run/react"; import { useMemo } from "react"; import type { User } from "~/models/user.server"; const DEFAULT_REDIRECT = "/"; /** * This should be used any time the redirect path is user-provided * (Like the query string on our login/signup pages). This avoids * open-redirect vulnerabilities. * @param {string} to The redirect destination * @param {string} defaultRedirect The redirect to use if the to is unsafe. */ export function safeRedirect( to: FormDataEntryValue | string | null | undefined, defaultRedirect: string = DEFAULT_REDIRECT ) { if (!to || typeof to !== "string") { return defaultRedirect; } if (!to.startsWith("/") || to.startsWith("//")) { return defaultRedirect; } return to; } export function getStationUrl(id: string, tag?: {slug: string}): string { if (tag) { return `/listen/tag/${tag?.slug}?station=${id}`; } return `/listen?station=${id}`; } /** * This base hook is used in other hooks to quickly search for specific data * across all loader data using useMatches. * @param {string} id The route id * @returns {JSON|undefined} The router data or undefined if not found */ export function useMatchesData( id: string ): Record | undefined { const matchingRoutes = useMatches(); const route = useMemo( () => matchingRoutes.find((route) => route.id === id), [matchingRoutes, id] ); return route?.data; } function isUser(user: any): user is User { return user && typeof user === "object" && typeof user.email === "string"; } export function useOptionalUser(): User | undefined { const data = useMatchesData("root"); if (!data || !isUser(data.user)) { return undefined; } return data.user; } export function useUser(): User { const maybeUser = useOptionalUser(); if (!maybeUser) { throw new Error( "No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead." ); } return maybeUser; } export function validateEmail(email: unknown): email is string { return typeof email === "string" && email.length > 3 && email.includes("@"); } export function notFound(message?: string) { return new Response(null, { status: 404, statusText: ["Not Found", message].filter(Boolean).join(": ") }); } export function slugify(string: string): string { const a = "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;"; const b = "aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------"; const p = new RegExp(a.split("").join("|"), "g"); return string.toString().toLowerCase() .replace(/\s+/g, "-") // Replace spaces with - .replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters .replace(/&/g, "-and-") // Replace & with 'and' .replace(/[^\w\-]+/g, "") // Remove all non-word characters .replace(/\-\-+/g, "-") // Replace multiple - with single - .replace(/^-+/, "") // Trim - from start of text .replace(/-+$/, ""); // Trim - from end of text } export type ConvertDatesToStrings = T extends Date ? string : T extends Array ? ConvertDatesToStrings[] : T extends object ? { [K in keyof T]: ConvertDatesToStrings } : T;