diff --git a/apps/www/components/Blog/BlogFilters.tsx b/apps/www/components/Blog/BlogFilters.tsx index d1af47d54a..5ac6a55f1d 100644 --- a/apps/www/components/Blog/BlogFilters.tsx +++ b/apps/www/components/Blog/BlogFilters.tsx @@ -1,6 +1,6 @@ import { LOCAL_STORAGE_KEYS, useBreakpoint } from 'common' import { AnimatePresence, motion } from 'framer-motion' -import { startCase } from 'lodash' +import { startCase } from 'lib/helpers' import { useSearchParams } from 'next/navigation' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' diff --git a/apps/www/components/CustomerStories/CustomersFilters.tsx b/apps/www/components/CustomerStories/CustomersFilters.tsx index 589bf297bc..d566127e69 100644 --- a/apps/www/components/CustomerStories/CustomersFilters.tsx +++ b/apps/www/components/CustomerStories/CustomersFilters.tsx @@ -11,7 +11,7 @@ import { cn, } from 'ui' import { ChevronDown, X as CloseIcon } from 'lucide-react' -import startCase from 'lodash/startCase' +import { startCase } from 'lib/helpers' import { useBreakpoint } from 'common' interface Props { diff --git a/apps/www/components/CustomerStories/CutomsersSliderDesktop.tsx b/apps/www/components/CustomerStories/CutomsersSliderDesktop.tsx index 4f0a60d71c..146a43678d 100644 --- a/apps/www/components/CustomerStories/CutomsersSliderDesktop.tsx +++ b/apps/www/components/CustomerStories/CutomsersSliderDesktop.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { range } from 'lodash' +import { range } from 'lib/helpers' import { cn } from 'ui' import { CompositionCol } from '.' diff --git a/apps/www/components/Events/EventsFilters.tsx b/apps/www/components/Events/EventsFilters.tsx index 006af0b7f7..71c372c8d4 100644 --- a/apps/www/components/Events/EventsFilters.tsx +++ b/apps/www/components/Events/EventsFilters.tsx @@ -1,6 +1,6 @@ import { useBreakpoint } from 'common' import { AnimatePresence, motion } from 'framer-motion' -import { startCase } from 'lodash' +import { startCase } from 'lib/helpers' import { useSearchParams } from 'next/navigation' import { useRouter } from 'next/router' import Link from 'next/link' diff --git a/apps/www/components/Globe.tsx b/apps/www/components/Globe.tsx index 47ecce12c6..2303317b2c 100644 --- a/apps/www/components/Globe.tsx +++ b/apps/www/components/Globe.tsx @@ -1,7 +1,7 @@ import createGlobe from 'cobe' import { useCallback, useEffect, useRef } from 'react' import { useTheme } from 'next-themes' -import { debounce } from 'lodash' +import { debounce } from 'lib/helpers' const Globe = () => { const { resolvedTheme } = useTheme() diff --git a/apps/www/components/LaunchWeek/12/LW12Background.tsx b/apps/www/components/LaunchWeek/12/LW12Background.tsx index cdb950f57f..b48591986c 100644 --- a/apps/www/components/LaunchWeek/12/LW12Background.tsx +++ b/apps/www/components/LaunchWeek/12/LW12Background.tsx @@ -1,6 +1,6 @@ import React from 'react' import { cn } from 'ui' -import { range } from 'lodash' +import { range } from 'lib/helpers' interface Props { className?: string diff --git a/apps/www/components/LaunchWeek/13/Releases/LWStickyNav.tsx b/apps/www/components/LaunchWeek/13/Releases/LWStickyNav.tsx index 9c14450574..7f6904ee2c 100644 --- a/apps/www/components/LaunchWeek/13/Releases/LWStickyNav.tsx +++ b/apps/www/components/LaunchWeek/13/Releases/LWStickyNav.tsx @@ -13,7 +13,6 @@ import { isBrowser } from 'common' import SectionContainer from '~/components/Layouts/SectionContainer' import { ArrowUpRight, ChevronDown } from 'lucide-react' -import { startCase } from 'lodash' const LWXStickyNav: FC = () => { const days = mainDays() diff --git a/apps/www/components/Products/DataAPIsVisual.tsx b/apps/www/components/Products/DataAPIsVisual.tsx index 80f80c8bf2..f7e9ef038f 100644 --- a/apps/www/components/Products/DataAPIsVisual.tsx +++ b/apps/www/components/Products/DataAPIsVisual.tsx @@ -1,6 +1,6 @@ import React from 'react' import Image from 'next/image' -import { range } from 'lodash' +import { range } from 'lib/helpers' import { cn } from 'ui' interface Props { diff --git a/apps/www/components/Products/StorageVisual.tsx b/apps/www/components/Products/StorageVisual.tsx index 18a48a3bbd..e3ddee4bfa 100644 --- a/apps/www/components/Products/StorageVisual.tsx +++ b/apps/www/components/Products/StorageVisual.tsx @@ -1,4 +1,4 @@ -import { range } from 'lodash' +import { range } from 'lib/helpers' import { File, Image, Video } from 'lucide-react' import { cn } from 'ui' diff --git a/apps/www/components/Sections/TwitterSocialProof.tsx b/apps/www/components/Sections/TwitterSocialProof.tsx index 9b7ead8f78..ec9d6a33ee 100644 --- a/apps/www/components/Sections/TwitterSocialProof.tsx +++ b/apps/www/components/Sections/TwitterSocialProof.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { cn } from 'ui' import { TweetCard } from 'ui-patterns/TweetCard' -import { range } from 'lodash' +import { range } from 'lib/helpers' import Tweets from '~/data/tweets/Tweets.json' import { useBreakpoint } from 'common' diff --git a/apps/www/components/logos.tsx b/apps/www/components/logos.tsx index 5c5608eb2b..6388265952 100644 --- a/apps/www/components/logos.tsx +++ b/apps/www/components/logos.tsx @@ -1,5 +1,5 @@ import { cn } from 'ui' -import { range } from 'lodash' +import { range } from 'lib/helpers' interface Props { showHeading?: boolean diff --git a/apps/www/lib/helpers.tsx b/apps/www/lib/helpers.tsx index c94a49722f..fb70995ed9 100644 --- a/apps/www/lib/helpers.tsx +++ b/apps/www/lib/helpers.tsx @@ -44,3 +44,162 @@ export const stripEmojis = (str: string) => ) .replace(/\s+/g, ' ') .trim() + +// Vanilla JavaScript implementations to replace lodash functions + +/** + * Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end. + * @param start The start of the range + * @param end The end of the range + * @param step The value to increment or decrement by + * @returns Returns the range of numbers + */ +export const range = (start: number, end?: number, step: number = 1): number[] => { + if (end === undefined) { + end = start + start = 0 + } + + const result: number[] = [] + for (let i = start; step > 0 ? i < end : i > end; i += step) { + result.push(i) + } + return result +} + +/** + * Converts string to start case. + * @param string The string to convert + * @returns Returns the start cased string + */ +export const startCase = (string: string): string => { + if (!string) return string + + return string + .replace(/[-_\s]+/g, ' ') + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/\b\w/g, (char) => char.toUpperCase()) + .trim() +} + +/** + * Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked. + * @param func The function to debounce + * @param wait The number of milliseconds to delay + * @param options The options object + * @returns Returns the new debounced function + */ +export const debounce = any>( + func: T, + wait: number, + options: { leading?: boolean; trailing?: boolean } = {} +): ((...args: Parameters) => void) & { + cancel: () => void + flush: () => void | undefined + pending: () => boolean +} => { + let timeoutId: ReturnType | undefined + let lastCallTime: number | undefined + let lastInvokeTime = 0 + + const { leading = false, trailing = true } = options + + function invokeFunc(time: number, ...args: Parameters) { + lastInvokeTime = time + func.apply(null, args) + } + + function startTimer(pendingFunc: () => void, wait: number) { + return setTimeout(pendingFunc, wait) + } + + function cancelTimer(id: ReturnType) { + clearTimeout(id) + } + + function leadingEdge(time: number, ...args: Parameters) { + lastInvokeTime = time + timeoutId = startTimer(timerExpired, wait) + return leading ? invokeFunc(time, ...args) : undefined + } + + function remainingWait(time: number) { + const timeSinceLastCall = time - (lastCallTime || 0) + const timeSinceLastInvoke = time - lastInvokeTime + const timeWaiting = wait - timeSinceLastCall + + return Math.min(timeWaiting, wait - timeSinceLastInvoke) + } + + function shouldInvoke(time: number) { + const timeSinceLastCall = time - (lastCallTime || 0) + const timeSinceLastInvoke = time - lastInvokeTime + + return ( + lastCallTime === undefined || + timeSinceLastCall >= wait || + timeSinceLastCall < 0 || + timeSinceLastInvoke >= wait + ) + } + + function timerExpired() { + const time = Date.now() + if (shouldInvoke(time)) { + return trailingEdge(time) + } + timeoutId = startTimer(timerExpired, remainingWait(time)) + } + + function trailingEdge(time: number) { + timeoutId = undefined + + if (trailing) { + // For trailing edge, we don't have the original arguments, so we call without them + return invokeFunc(time, ...([] as any)) + } + } + + function cancel() { + if (timeoutId !== undefined) { + cancelTimer(timeoutId) + } + lastInvokeTime = 0 + lastCallTime = undefined + timeoutId = undefined + } + + function flush() { + return timeoutId === undefined ? undefined : trailingEdge(Date.now()) + } + + function pending() { + return timeoutId !== undefined + } + + function debounced(this: any, ...args: Parameters) { + const time = Date.now() + const isInvoking = shouldInvoke(time) + + lastCallTime = time + + if (isInvoking) { + if (timeoutId === undefined) { + return leadingEdge(lastCallTime, ...args) + } + if (trailing) { + timeoutId = startTimer(timerExpired, wait) + return invokeFunc(lastCallTime, ...args) + } + } + if (timeoutId === undefined) { + timeoutId = startTimer(timerExpired, wait) + } + } + + debounced.cancel = cancel + debounced.flush = flush + debounced.pending = pending + + return debounced +} diff --git a/apps/www/package.json b/apps/www/package.json index 6f63357f1c..8cfc903c24 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -49,7 +49,6 @@ "globby": "^13.2.2", "gray-matter": "^4.0.3", "icons": "workspace:*", - "lodash": "^4.0.0", "lucide-react": "*", "markdown-toc": "^1.2.0", "next": "catalog:", @@ -87,7 +86,6 @@ "@types/animejs": "^3.1.12", "@types/classnames": "^2.3.1", "@types/common-tags": "^1.8.4", - "@types/lodash": "^4.0.0", "@types/mdx-js__react": "^1.5.6", "@types/parse-numeric-range": "^0.0.1", "@types/react": "catalog:", diff --git a/apps/www/pages/blog/categories/[category].tsx b/apps/www/pages/blog/categories/[category].tsx index 1abfdd0621..2251a159f4 100644 --- a/apps/www/pages/blog/categories/[category].tsx +++ b/apps/www/pages/blog/categories/[category].tsx @@ -1,7 +1,7 @@ import { NextSeo } from 'next-seo' import { getSortedPosts, getAllCategories } from '~/lib/posts' import Link from 'next/link' -import { startCase } from 'lodash' +import { startCase } from 'lib/helpers' import DefaultLayout from '~/components/Layouts/Default' import BlogGridItem from '~/components/Blog/BlogGridItem' diff --git a/apps/www/pages/blog/tags/[tag].tsx b/apps/www/pages/blog/tags/[tag].tsx index f6fcad0f2e..59efb04edf 100644 --- a/apps/www/pages/blog/tags/[tag].tsx +++ b/apps/www/pages/blog/tags/[tag].tsx @@ -1,7 +1,7 @@ import { NextSeo } from 'next-seo' import { getSortedPosts, getAllTags } from '~/lib/posts' import Link from 'next/link' -import { startCase } from 'lodash' +import { startCase } from 'lib/helpers' import DefaultLayout from '~/components/Layouts/Default' import BlogGridItem from '~/components/Blog/BlogGridItem' diff --git a/apps/www/pages/events/[slug].tsx b/apps/www/pages/events/[slug].tsx index 4812128cfc..4bd7f78f2f 100644 --- a/apps/www/pages/events/[slug].tsx +++ b/apps/www/pages/events/[slug].tsx @@ -6,7 +6,6 @@ import { } from '@heroicons/react/solid' import dayjs from 'dayjs' import matter from 'gray-matter' -import capitalize from 'lodash/capitalize' import { ChevronLeft, X as XIcon } from 'lucide-react' import { MDXRemote } from 'next-mdx-remote' import { NextSeo } from 'next-seo' @@ -14,7 +13,7 @@ import NextImage from 'next/image' import Link from 'next/link' import authors from 'lib/authors.json' -import { isNotNullOrUndefined } from '~/lib/helpers' +import { capitalize, isNotNullOrUndefined } from '~/lib/helpers' import mdxComponents from '~/lib/mdx/mdxComponents' import { mdxSerialize } from '~/lib/mdx/mdxSerialize' import { getAllPostSlugs, getPostdata } from '~/lib/posts' diff --git a/apps/www/pages/features.tsx b/apps/www/pages/features.tsx index 40a0f5d60c..7286a4e7e1 100644 --- a/apps/www/pages/features.tsx +++ b/apps/www/pages/features.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/router' import { NextSeo } from 'next-seo' import { motion } from 'framer-motion' import { Search } from 'lucide-react' -import debounce from 'lodash/debounce' +import { debounce } from 'lib/helpers' import { Button, Checkbox, cn, Input } from 'ui' import DefaultLayout from '~/components/Layouts/Default' diff --git a/apps/www/pages/partners/index.tsx b/apps/www/pages/partners/index.tsx index 56be04814d..10e928bf3e 100644 --- a/apps/www/pages/partners/index.tsx +++ b/apps/www/pages/partners/index.tsx @@ -1,4 +1,4 @@ -import { range } from 'lodash' +import { range } from 'lib/helpers' import { NextSeo } from 'next-seo' import Image from 'next/image' import Link from 'next/link' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3056b2a9d..8b3018eca7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1161,7 +1161,7 @@ importers: version: 4.4.2 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@22.13.14)(typescript@5.5.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.4.5)) '@vitest/ui': specifier: ^3.0.0 version: 3.0.4(vitest@3.0.9) @@ -1607,9 +1607,6 @@ importers: icons: specifier: workspace:* version: link:../../packages/icons - lodash: - specifier: ^4.0.0 - version: 4.17.21 lucide-react: specifier: '*' version: 0.436.0(react@18.3.1) @@ -1716,9 +1713,6 @@ importers: '@types/common-tags': specifier: ^1.8.4 version: 1.8.4 - '@types/lodash': - specifier: ^4.0.0 - version: 4.17.5 '@types/mdx-js__react': specifier: ^1.5.6 version: 1.5.6 @@ -1930,7 +1924,7 @@ importers: version: 18.3.0 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@22.13.14)(typescript@5.5.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.4.5)) '@vitest/ui': specifier: ^3.0.0 version: 3.0.4(vitest@3.0.9) @@ -10936,7 +10930,6 @@ packages: resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==} peerDependencies: eslint: '>= 5' - bundledDependencies: [] eslint-plugin-import@2.31.0: resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} @@ -15012,10 +15005,6 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} - punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -27216,24 +27205,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0(supports-color@8.1.1) - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.8.1 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5) - transitivePeerDependencies: - - supports-color - '@vitest/expect@3.0.9': dependencies: '@vitest/spy': 3.0.9 @@ -27300,7 +27271,7 @@ snapshots: sirv: 3.0.0 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5) + vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@22.13.14)(typescript@5.5.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.4.5) '@vitest/utils@3.0.4': dependencies: @@ -34867,8 +34838,6 @@ snapshots: punycode.js@2.3.1: {} - punycode@2.3.0: {} - punycode@2.3.1: {} qs-esm@7.0.2: {} @@ -37838,7 +37807,7 @@ snapshots: uri-js@4.4.1: dependencies: - punycode: 2.3.0 + punycode: 2.3.1 url-parse@1.5.10: dependencies: