diff --git a/apps/frontend/app/(website)/i18n/page.tsx b/apps/frontend/app/(website)/i18n/page.tsx new file mode 100644 index 000000000..a8e25f192 --- /dev/null +++ b/apps/frontend/app/(website)/i18n/page.tsx @@ -0,0 +1,41 @@ +import { I18nPage } from "@/components/templates/i18nPage/Page"; +import I18NPagePreview from "@/components/templates/i18nPage/PagePreview"; +import { loadSanityPageByRouteProps } from "@/data/sanity"; +import { resolveSanityRouteMetadata } from "@/data/sanity/resolveSanityRouteMetadata"; +import type { RouteProps } from "@/types"; +import type { ResolvingMetadata } from "next"; +import { draftMode } from "next/headers"; +import { notFound } from "next/navigation"; + +export async function generateMetadata( + props: RouteProps, + parent: ResolvingMetadata, +) { + const initialData = await loadSanityPageByRouteProps({ + params: { + path: ["i18n"], + locale: "", + }, + }); + + if (!initialData?.data) return notFound(); + + return resolveSanityRouteMetadata(initialData.data, parent); +} + +export default async function I18n() { + const initial = await loadSanityPageByRouteProps({ + params: { + path: ["i18n"], + locale: "", + }, + }); + + if (!initial?.data) return notFound(); + + if (draftMode().isEnabled) { + return ; + } + + return ; +} diff --git a/apps/frontend/components/global/Navigation/index.tsx b/apps/frontend/components/global/Navigation/index.tsx index ba2bf6621..3ff3621e1 100644 --- a/apps/frontend/components/global/Navigation/index.tsx +++ b/apps/frontend/components/global/Navigation/index.tsx @@ -78,7 +78,7 @@ export default function Navigation({ data }: NavigationProps) { ref={headerRef} id={"desktop-navigation"} className={cx( - "left-0 top-0 z-40 w-full border-b-[1px] transition-all duration-150 ease-out z-10000", + "left-0 top-0 z-[80] w-full border-b-[1px] transition-all duration-150 ease-out z-10000", isStaticHeader ? "absolute" : "fixed", { "-translate-y-10 opacity-0": !hasMounted, diff --git a/apps/frontend/components/shared/pt.blocks/Quote.tsx b/apps/frontend/components/shared/pt.blocks/Quote.tsx index 02e8b71a2..45fdeea99 100644 --- a/apps/frontend/components/shared/pt.blocks/Quote.tsx +++ b/apps/frontend/components/shared/pt.blocks/Quote.tsx @@ -1,40 +1,110 @@ +"use client"; +import TextRotate from "@/components/templates/i18nPage/Cobe/TextRotate"; import type { QuoteProps } from "@/types/object.types"; +import { LayoutGroup, motion, useInView } from "motion/react"; +import { useRef } from "react"; import { SanityImage } from "../SanityImage"; export const Quote = (props: QuoteProps) => { + const blockquoteRef = useRef(null); + const inView = useInView(blockquoteRef, { + once: true, + margin: "0% 0%", + }); + return ( -
- {props.image && ( - - )} - - {props.quote} - -
- {props.authorImage && ( - +
+ + {props.quote && ( + + + )} -
- - {props.authorName} - - {props.authorPosition && ( - - {props.authorPosition} - - )} +
+ + {props.authorImage && ( + + )} + {props.image && ( + + )} + +
+ {props.authorName && ( + + + + )} + + {props.authorPosition && ( + + + + )} +
-
+
); }; diff --git a/apps/frontend/components/templates/BlogArticlePage/BlogArticlePageContent.tsx b/apps/frontend/components/templates/BlogArticlePage/BlogArticlePageContent.tsx index 6306cfe95..a1171606d 100644 --- a/apps/frontend/components/templates/BlogArticlePage/BlogArticlePageContent.tsx +++ b/apps/frontend/components/templates/BlogArticlePage/BlogArticlePageContent.tsx @@ -75,7 +75,7 @@ export default function BlogArticlePageContent(props: BlogArticlePayload) { )}
-

+

{props?.title}

@@ -112,7 +112,7 @@ export default function BlogArticlePageContent(props: BlogArticlePayload) {
{/* Body */} -
+
{typeof props.readTime === "number" && ( diff --git a/apps/frontend/components/templates/i18nPage/Cobe/Globe/index.tsx b/apps/frontend/components/templates/i18nPage/Cobe/Globe/index.tsx new file mode 100644 index 000000000..75884c728 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Cobe/Globe/index.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { cn } from "@/utils"; +import createGlobe, { type COBEOptions } from "cobe"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useThemeDetector } from "../../Preview/hooks"; + +const LIGHT_THEME_CONFIG: Partial = { + dark: 0, + baseColor: [255 / 255, 255 / 255, 255 / 255], // #FFF in normalized RGB + markerColor: [137 / 255, 185 / 255, 0 / 255], // #89B900 in normalized RGB + glowColor: [247 / 255, 255 / 255, 223 / 255], // #F7FFDF in normalized RGB +}; + +const DARK_THEME_CONFIG: Partial = { + dark: 1, + baseColor: [53 / 255, 69 / 255, 85 / 255], // #354555 in normalized RGB + markerColor: [214 / 255, 255 / 255, 98 / 255], // #D6FF62 in normalized RGB + glowColor: [12 / 255, 22 / 255, 31 / 255], // #0C161F in normalized RGB +}; + +const GLOBE_CONFIG: Partial = { + width: 800, + height: 800, + onRender: () => {}, + devicePixelRatio: 2, + phi: 0, + theta: 0.3, + diffuse: 1.75, + mapSamples: 16000, + mapBrightness: 7, + markers: [ + { location: [14.5995, 120.9842], size: 0.03 }, + { location: [19.076, 72.8777], size: 0.1 }, + { location: [23.8103, 90.4125], size: 0.05 }, + { location: [30.0444, 31.2357], size: 0.07 }, + { location: [39.9042, 116.4074], size: 0.08 }, + { location: [-23.5505, -46.6333], size: 0.1 }, + { location: [19.4326, -99.1332], size: 0.1 }, + { location: [40.7128, -74.006], size: 0.1 }, + { location: [34.6937, 135.5022], size: 0.05 }, + { location: [41.0082, 28.9784], size: 0.06 }, + ], +}; + +export function Globe({ + className, + config = GLOBE_CONFIG, +}: { + className?: string; + config?: Partial; +}) { + let phi = 0; + let width = 0; + const canvasRef = useRef(null); + const pointerInteracting = useRef(null); + const pointerInteractionMovement = useRef(0); + const [r, setR] = useState(0); + const theme = useThemeDetector(); + + // Dynamically apply theme-based config + const _config = { + ...(theme === "dark" ? DARK_THEME_CONFIG : LIGHT_THEME_CONFIG), + ...config, + } as COBEOptions; + + const updateMovement = (clientX: number) => { + if (pointerInteracting.current !== null) { + const delta = clientX - pointerInteracting.current; + pointerInteractionMovement.current = delta; + setR(delta / 200); + } + }; + + const onRender = useCallback( + (state: Record) => { + if (!pointerInteracting.current) phi += 0.005; + state.phi = phi + r; + state.width = width * 2; + state.height = width * 2; + }, + [r], + ); + + const onResize = () => { + if (canvasRef.current) { + width = canvasRef.current.offsetWidth; + } + }; + + useEffect(() => { + window.addEventListener("resize", onResize); + onResize(); + + const globe = createGlobe(canvasRef.current!, { + ..._config, + width: width * 2, + height: width * 2, + onRender, + }); + + setTimeout(() => (canvasRef.current!.style.opacity = "1"), 300); + return () => globe.destroy(); + }, [_config]); + + return ( +
+ updateMovement(e.clientX)} + onTouchMove={(e) => + e.touches[0] && updateMovement(e.touches[0].clientX) + } + /> +
+ ); +} diff --git a/apps/frontend/components/templates/i18nPage/Cobe/NumberTicker/index.tsx b/apps/frontend/components/templates/i18nPage/Cobe/NumberTicker/index.tsx new file mode 100644 index 000000000..4805ea0f8 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Cobe/NumberTicker/index.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { cn } from "@/utils"; +import { useInView, useMotionValue, useSpring } from "framer-motion"; +import { useEffect, useRef } from "react"; + +export function NumberTicker({ + value, + direction = "up", + delay = 0, + className, + decimalPlaces = 0, + suffix = "", +}: { + value: number; + direction?: "up" | "down"; + className?: string; + delay?: number; // delay in seconds + decimalPlaces?: number; + suffix?: string; // suffix text +}) { + const ref = useRef(null); + const motionValue = useMotionValue(direction === "down" ? value : 0); + const springValue = useSpring(motionValue, { + damping: 60, + stiffness: 100, + }); + const isInView = useInView(ref, { once: true, margin: "0px" }); + + useEffect(() => { + if (isInView) { + setTimeout(() => { + motionValue.set(direction === "down" ? 0 : value); + }, delay * 1000); + } + }, [motionValue, isInView, delay, value, direction]); + + useEffect(() => { + const unsubscribe = springValue.on("change", (latest) => { + if (ref.current) { + ref.current.textContent = `${Intl.NumberFormat("en-US", { + minimumFractionDigits: decimalPlaces, + maximumFractionDigits: decimalPlaces, + }).format(Number(latest.toFixed(decimalPlaces)))}${suffix}`; + } + }); + + return () => unsubscribe(); + }, [springValue, decimalPlaces, suffix]); + + return ( + + ); +} diff --git a/apps/frontend/components/templates/i18nPage/Cobe/TextRotate/index.tsx b/apps/frontend/components/templates/i18nPage/Cobe/TextRotate/index.tsx new file mode 100644 index 000000000..ce7af4434 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Cobe/TextRotate/index.tsx @@ -0,0 +1,266 @@ +"use client"; + +import { cn } from "@/utils"; +import { + AnimatePresence, + type AnimatePresenceProps, + type MotionProps, + type Transition, + motion, +} from "motion/react"; +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from "react"; + +interface TextRotateProps { + texts: string[]; + rotationInterval?: number; + initial?: MotionProps["initial"]; + animate?: MotionProps["animate"]; + exit?: MotionProps["exit"]; + animatePresenceMode?: AnimatePresenceProps["mode"]; + animatePresenceInitial?: boolean; + staggerDuration?: number; + staggerFrom?: "first" | "last" | "center" | number | "random"; + transition?: Transition; + loop?: boolean; // Whether to start from the first text when the last one is reached + auto?: boolean; // Whether to start the animation automatically + splitBy?: "words" | "characters" | "lines" | string; + onNext?: (index: number) => void; + mainClassName?: string; + splitLevelClassName?: string; + elementLevelClassName?: string; +} + +export interface TextRotateRef { + next: () => void; + previous: () => void; + jumpTo: (index: number) => void; + reset: () => void; +} + +interface WordObject { + characters: string[]; + needsSpace: boolean; +} + +const TextRotate = forwardRef( + ( + { + texts, + transition = { type: "spring", damping: 25, stiffness: 300 }, + initial = { y: "100%", opacity: 0 }, + animate = { y: 0, opacity: 1 }, + exit = { y: "-120%", opacity: 0 }, + animatePresenceMode = "wait", + animatePresenceInitial = false, + rotationInterval = 2000, + staggerDuration = 0, + staggerFrom = "first", + loop = true, + auto = true, + splitBy = "characters", + onNext, + mainClassName, + splitLevelClassName, + elementLevelClassName, + ...props + }, + ref, + ) => { + const [currentTextIndex, setCurrentTextIndex] = useState(0); + + // handy function to split text into characters with support for unicode and emojis + const splitIntoCharacters = (text: string): string[] => { + if (typeof Intl !== "undefined" && "Segmenter" in Intl) { + const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); + return Array.from(segmenter.segment(text), ({ segment }) => segment); + } + // Fallback for browsers that don't support Intl.Segmenter + return Array.from(text); + }; + + const elements = useMemo(() => { + const currentText = texts[currentTextIndex]; + if (splitBy === "characters") { + const text = currentText?.split(" "); + return text?.map((word, i) => ({ + characters: splitIntoCharacters(word), + needsSpace: i !== text?.length - 1, + })); + } + return splitBy === "words" + ? currentText?.split(" ") + : splitBy === "lines" + ? currentText?.split("\n") + : currentText?.split(splitBy); + }, [texts, currentTextIndex, splitBy]); + + const getStaggerDelay = useCallback( + (index: number, totalChars: number) => { + const total = totalChars; + if (staggerFrom === "first") return index * staggerDuration; + if (staggerFrom === "last") + return (total - 1 - index) * staggerDuration; + if (staggerFrom === "center") { + const center = Math.floor(total / 2); + return Math.abs(center - index) * staggerDuration; + } + if (staggerFrom === "random") { + const randomIndex = Math.floor(Math.random() * total); + return Math.abs(randomIndex - index) * staggerDuration; + } + return Math.abs(staggerFrom - index) * staggerDuration; + }, + [staggerFrom, staggerDuration], + ); + + // Helper function to handle index changes and trigger callback + const handleIndexChange = useCallback( + (newIndex: number) => { + setCurrentTextIndex(newIndex); + onNext?.(newIndex); + }, + [onNext], + ); + + const next = useCallback(() => { + const nextIndex = + currentTextIndex === texts.length - 1 + ? loop + ? 0 + : currentTextIndex + : currentTextIndex + 1; + + if (nextIndex !== currentTextIndex) { + handleIndexChange(nextIndex); + } + }, [currentTextIndex, texts.length, loop, handleIndexChange]); + + const previous = useCallback(() => { + const prevIndex = + currentTextIndex === 0 + ? loop + ? texts.length - 1 + : currentTextIndex + : currentTextIndex - 1; + + if (prevIndex !== currentTextIndex) { + handleIndexChange(prevIndex); + } + }, [currentTextIndex, texts.length, loop, handleIndexChange]); + + const jumpTo = useCallback( + (index: number) => { + const validIndex = Math.max(0, Math.min(index, texts.length - 1)); + if (validIndex !== currentTextIndex) { + handleIndexChange(validIndex); + } + }, + [texts.length, currentTextIndex, handleIndexChange], + ); + + const reset = useCallback(() => { + if (currentTextIndex !== 0) { + handleIndexChange(0); + } + }, [currentTextIndex, handleIndexChange]); + + // Expose all navigation functions via ref + useImperativeHandle( + ref, + () => ({ + next, + previous, + jumpTo, + reset, + }), + [next, previous, jumpTo, reset], + ); + + useEffect(() => { + if (!auto) return; + const intervalId = setInterval(next, rotationInterval); + return () => clearInterval(intervalId); + }, [next, rotationInterval, auto]); + + return ( + + {texts[currentTextIndex]} + + + + + + ); + }, +); + +TextRotate.displayName = "TextRotate"; + +export default TextRotate; diff --git a/apps/frontend/components/templates/i18nPage/Cobe/TextTransitions/index.tsx b/apps/frontend/components/templates/i18nPage/Cobe/TextTransitions/index.tsx new file mode 100644 index 000000000..22175ca42 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Cobe/TextTransitions/index.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { LayoutGroup, motion } from "motion/react"; +import TextRotate from "../TextRotate"; + +const TextTransitions = () => { + return ( + + + + Go Global, at Lightning Speed{" "} + + + + + ); +}; + +export default TextTransitions; diff --git a/apps/frontend/components/templates/i18nPage/Cobe/index.tsx b/apps/frontend/components/templates/i18nPage/Cobe/index.tsx new file mode 100644 index 000000000..b1d067eae --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Cobe/index.tsx @@ -0,0 +1,41 @@ +import { Globe } from "./Globe"; +import { NumberTicker } from "./NumberTicker"; +import TextTransitions from "./TextTransitions"; + +const CobeSection = () => { + return ( +
+
+
+ + +
+
+

Time to market

+

+ +

+

Faster time to market

+
+
+

Revenue Growth

+

+ +

+

More revenue potential

+
+
+

+ Enter the global market faster and drive unprecedented business + growth, all while freeing your engineering team from thousands of + tedious code changes. Let engineers focus on building core product + features, not preparing the app for internationalization. +

+
+ +
+
+ ); +}; + +export default CobeSection; diff --git a/apps/frontend/components/templates/i18nPage/I18nPageContent.tsx b/apps/frontend/components/templates/i18nPage/I18nPageContent.tsx new file mode 100644 index 000000000..9e3e9adfe --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/I18nPageContent.tsx @@ -0,0 +1,93 @@ +import GradientBlob from "@/components/shared/GradientBlob"; +import GradientBorderBox from "@/components/shared/GradientBorderBox"; +import InfiniteSlider from "@/components/shared/InfiniteSlider"; +import LinkButton from "@/components/shared/LinkButton"; +import BookOpenLinkButton from "@/components/shared/animated-icons/BookOpenLinkButton"; +import CobeSection from "./Cobe"; +import type { I18NPageProps } from "./Page"; +import DemoSection from "./Preview/DemoSection"; +import ShiningLines from "./Preview/ShinningLines"; + +export default function I18NPageSections({ data }: I18NPageProps) { + return ( + <> + {/* Hero */} +
+
+
+ + +
+
+ + + +
+
+

+ {data?.hero?.title} +

+ {data?.hero?.subtitle && ( +

+ {data?.hero?.subtitle} +

+ )} +
+
+ {data.hero?.ctas?.[0] && ( + + {data.hero?.ctas?.[0].label} + + )} + {data.hero?.ctas?.[1] && ( + <> + + {data.hero?.ctas?.[1].label} + + + )} +
+
+
+ + + + {data?.hero?.logoCarousel?.logos?.length && ( +
+ +
+
+

+ {data?.hero?.logoCarousel.title} +

+ {data?.hero?.logoCarousel?.logos && ( + + )} +
+
+
+
+ )} + + + ); +} diff --git a/apps/frontend/components/templates/i18nPage/Page.tsx b/apps/frontend/components/templates/i18nPage/Page.tsx new file mode 100644 index 000000000..49e9b5875 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Page.tsx @@ -0,0 +1,24 @@ +import { SectionsRenderer } from "@/components/SectionsRenderer"; +import PageCta from "@/components/templates/ModularPage/PageCta"; +import type { ModularPagePayload } from "@/types"; +import { sections as sectionComponents } from "../../sections/sections.server"; +import I18nPageContent from "./I18nPageContent"; + +export interface I18NPageProps { + data: ModularPagePayload; +} + +export function I18nPage({ data }: I18NPageProps) { + return ( +
+ + + {data?.cta && } +
+ ); +} diff --git a/apps/frontend/components/templates/i18nPage/PagePreview.tsx b/apps/frontend/components/templates/i18nPage/PagePreview.tsx new file mode 100644 index 000000000..61590fccc --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/PagePreview.tsx @@ -0,0 +1,43 @@ +"use client"; + +import type { QueryResponseInitial } from "@sanity/react-loader"; + +import { useQuery } from "@/data/sanity/useQuery"; +import type { ModularPagePayload } from "@/types"; + +import PageCta from "@/components/templates/ModularPage/PageCta"; +import { PAGE_QUERY } from "@/data/sanity/queries"; +import { SectionsRenderer } from "../../SectionsRenderer"; +import { sections } from "../../sections/sections.preview"; +import I18nPageContent from "./I18nPageContent"; + +type PreviewRouteProps = { + initial: QueryResponseInitial; +}; + +export default function I18NPagePreview(props: PreviewRouteProps) { + const { initial } = props; + + const { data } = useQuery( + PAGE_QUERY, + { + pathname: "i18n", + locale: "en", + }, + { + initial, + }, + ); + + return ( +
+ {data && } + + {data?.cta && } +
+ ); +} diff --git a/apps/frontend/components/templates/i18nPage/Preview/CircularProgress/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/CircularProgress/index.tsx new file mode 100644 index 000000000..81e04b427 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CircularProgress/index.tsx @@ -0,0 +1,58 @@ +import { motion } from "framer-motion"; + +interface CircularProgressProps { + size: number; + strokeWidth: number; + percentage: number; + className?: string; +} + +const CircularProgress = ({ + size, + strokeWidth, + percentage, + className = "text-lime-500 dark:text-accent", +}: CircularProgressProps) => { + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + + return ( +
+ + {/* Track */} + + {/* Progress */} + + +
+ ); +}; + +export default CircularProgress; diff --git a/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/Code.tsx b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/Code.tsx new file mode 100644 index 000000000..5c3f2adcd --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/Code.tsx @@ -0,0 +1,29 @@ +"use client"; +import type { HighlightedCode } from "codehike/code"; +import { Pre } from "codehike/code"; +import { className } from "./classname"; +import { tokenTransitions } from "./token-transitions"; + +export function CodeSwitcher({ + info, +}: { + info: HighlightedCode; +}) { + return ( +
+ + /app/login/page.tsx + +
+    
+ ); +} diff --git a/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/JSONHighlighter.tsx b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/JSONHighlighter.tsx new file mode 100644 index 000000000..6cca3ad4a --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/JSONHighlighter.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { type HighlightedCode, Pre } from "codehike/code"; +import type React from "react"; +import { memo } from "react"; +import { Steps } from "../types"; +import { tokenTransitions } from "./token-transitions"; + +interface JsonHighlighterProps { + step: number; + code: HighlightedCode | null; +} + +const JsonHighlighter: React.FC = ({ step, code }) => { + if (!code) { + return
No code available
; + } + + return ( +
+ {/* File Path Label */} + + /app/locales/{step > Steps.Translating ? "es-ES" : "en-US"}.json + + {/* JSON Code Viewer */} +
+    
+ ); +}; + +export default memo(JsonHighlighter); diff --git a/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/classname.tsx b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/classname.tsx new file mode 100644 index 000000000..24c79d694 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/classname.tsx @@ -0,0 +1,11 @@ +import type { AnnotationHandler } from "codehike/code"; + +export const className: AnnotationHandler = { + name: "className", + Block: ({ annotation, children }) => { + return
{children}
; + }, + Inline: ({ annotation, children }) => { + return {children}; + }, +}; diff --git a/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/constants.ts b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/constants.ts new file mode 100644 index 000000000..f848d5890 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/constants.ts @@ -0,0 +1,102 @@ +export const JSON_STEP_1 = `{ + "create-an-account": "Create an account", + "enter-your-email-below": "Enter your email below to create your account", + "enter-your-email": "Enter your email...", + "sign-in-with-email": "Sign in with email", + "or-continue-with": "Or continue with", + "github": "Github", + "sign-up-message": "¿No tienes una cuenta? {link}", + "sign-up-link": "Regístrate", +}`; + +export const JSON_STEP_2 = `{ + "create-an-account": "Crea una cuenta", + "enter-your-email-below": "Ingresa tu correo electrónico abajo para crear tu cuenta", + "enter-your-email": "Introduce tu correo electrónico...", + "sign-in-with-email": "Inicia sesión con tu correo", + "or-continue-with": "O continúa con", + "github": "Github", + "sign-up-message": "¿No tienes una cuenta? {link}", + "sign-up-link": "Regístrate", +}`; + +export const CODE_STEP_1 = `export function LoginPage() { + return ( + + Create an account + Enter your email below to create your account +
+ + +
+ + Or continue with + + +
+ Don't have an account?{" "} + Sign up +
+
+ ); +}`; + +export const CODE_STEP_2 = `export function LoginPage() { + return ( + + {/* !className[/Create an account/] outline !outline-red-500 */} + Create an account + {/* !className[/Enter your email below to create your account/] outline !outline-green-500 */} + Enter your email below to create your account +
+ {/* !className[/Enter your email.../] outline !outline-blue-500 */} + + {/* !className[/Sign In with Email/] outline !outline-yellow-500 */} + +
+ + {/* !className[/Or continue with/] outline !outline-purple-500 */} + Or continue with + + {/* !className[/GitHub/] outline !outline-orange-500 */} + +
+ {/* !className[/D.+\\}/] outline !outline-lime-500 */} + Don't have an account?{" "} + {/* !className[//] outline !outline-cyan-500 */} + Sign up +
+
+ ); +}`; + +export const CODE_STEP_3 = `export function LoginPage() { + return ( + + {/* !className[/\{t\(.+\)\}/] outline !outline-red-500 */} + {t('create-an-account')} + {/* !className[/\{t\(.+\)\}/] outline !outline-green-500 */} + {t('enter-your-email-below')} +
+ {/* !className[/\{t\(.+\)\}/] outline !outline-blue-500 */} + + {/* !className[/\{t\(.+\)\}/] outline !outline-yellow-500 */} + +
+ + {/* !className[/\{t\(.+\)\}/] outline !outline-purple-500 */} + {t('or-continue-with')} + + {/* !className[/\{t\(.+\)\}/] outline !outline-orange-500 */} + +
+ {/* !className[/\\{t\\(.+/] outline !outline-lime-500 */} + {t("sign-up-message", { + {/* !className[/\\s{2}link.+a>/] outline !outline-cyan-500 */} + link: {t("sign-up-link")} + {/* !className[/\\S+/] outline !outline-rose-500 */} + })} +
+
+ ); +}`; diff --git a/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/index.tsx new file mode 100644 index 000000000..e4d4e7893 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/index.tsx @@ -0,0 +1,104 @@ +"use client"; + +import Scanner from "@/components/templates/i18nPage/Preview/ScannerEffect"; +import { Steps } from "@/components/templates/i18nPage/Preview/types"; +import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; +import { useCallback, useEffect, useRef, useState } from "react"; +import LoadingSpinner from "../Spinner"; +import { useHighlights } from "../hooks"; +import { CodeSwitcher } from "./Code"; +import JsonHighlighter from "./JSONHighlighter"; + +interface CodeProps { + step: Steps; + setStep: (newStep: Steps | ((prevStep: Steps) => Steps)) => void; + isAnimating: boolean; +} + +const DELAYS = [0, 2000, 3500]; +const Code = ({ step, setStep, isAnimating }: CodeProps) => { + const [JSXComplete, setJSXComplete] = useState(false); + const { infos, jsonInfos, loading } = useHighlights(); + const stepRef = useRef(step); + + useEffect(() => { + stepRef.current = step; + }, [step]); + + const handleNext = useCallback(() => { + if (!isAnimating) return; + const step = stepRef.current; + + setTimeout(() => { + setStep((prevStep) => Math.min(prevStep + 1, 3)); + }, DELAYS[step]); + }, [isAnimating, setStep]); + + if (loading) { + return ; + } + + const jsxIndex = Math.min(step, 2); + const jsonIndex = step >= 3 ? 1 : 0; + + return ( + <> + + {isAnimating && step === Steps.Analyzing && ( + + )} + + + + {infos && infos.length > 0 && ( + Steps.Transforming ? "60%" : "100%", + }} + exit={{ height: "100%" }} + transition={{ duration: 1.25, ease: "easeInOut" }} + onAnimationComplete={() => { + setJSXComplete(true); + }} + > + {infos[jsxIndex] && } + + )} + + + {JSXComplete && step > Steps.Transforming && ( + + {jsonInfos && ( + + )} + + )} + + + + ); +}; + +export default Code; diff --git a/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/token-transitions.client.tsx b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/token-transitions.client.tsx new file mode 100644 index 000000000..999184793 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/token-transitions.client.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { type CustomPreProps, InnerPre, getPreRef } from "codehike/code"; + +import { + type TokenTransitionsSnapshot, + calculateTransitions, + getStartingSnapshot, +} from "codehike/utils/token-transitions"; +import React from "react"; + +const MAX_TRANSITION_DURATION = 3000; // milliseconds + +export class PreWithRef extends React.Component { + ref: React.RefObject; + constructor(props: CustomPreProps) { + super(props); + this.ref = getPreRef(this.props); + } + + render() { + return ; + } + + getSnapshotBeforeUpdate() { + return getStartingSnapshot(this.ref.current!); + } + + componentDidUpdate( + prevProps: never, + prevState: never, + snapshot: TokenTransitionsSnapshot, + ) { + const transitions = calculateTransitions(this.ref.current!, snapshot); + transitions.forEach(({ element, keyframes, options }) => { + const { translateX, translateY, ...kf } = keyframes as any; + if (translateX && translateY) { + kf.translate = [ + `${translateX[0]}px ${translateY[0]}px`, + `${translateX[1]}px ${translateY[1]}px`, + ]; + } + + element.animate(kf, { + duration: 750 + options.duration * MAX_TRANSITION_DURATION, + delay: options.delay * MAX_TRANSITION_DURATION, + easing: options.easing, + fill: "both", + }); + }); + + setTimeout(() => { + if (this.props.onTransitionEnd) { + this.props.onTransitionEnd({} as any); + } + }, MAX_TRANSITION_DURATION); + } +} diff --git a/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/token-transitions.tsx b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/token-transitions.tsx new file mode 100644 index 000000000..24badbaf2 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/CodeSwitcher/token-transitions.tsx @@ -0,0 +1,10 @@ +import { type AnnotationHandler, InnerToken } from "codehike/code"; +import { PreWithRef } from "./token-transitions.client"; + +export const tokenTransitions: AnnotationHandler = { + name: "token-transitions", + PreWithRef, + Token: (props) => ( + + ), +}; diff --git a/apps/frontend/components/templates/i18nPage/Preview/DemoSection.tsx b/apps/frontend/components/templates/i18nPage/Preview/DemoSection.tsx new file mode 100644 index 000000000..070b97337 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/DemoSection.tsx @@ -0,0 +1,102 @@ +"use client"; +import Preview from "@/components/templates/i18nPage/Preview"; +import Code from "@/components/templates/i18nPage/Preview/CodeSwitcher"; +import { Play } from "@/components/templates/i18nPage/Preview/Play"; +import { Steps } from "@/components/templates/i18nPage/Preview/types"; +import { AnimatePresence, motion, useInView } from "framer-motion"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Timeline } from "./Timeline"; + +export default function DemoSection() { + const [step, setStep] = useState(Steps.Analyzing); + const [isAnimating, setIsAnimating] = useState(false); + const [resetKey, setResetKey] = useState(0); + const [hasPlayed, setHasPlayed] = useState(false); + + const ref = useRef(null); + const isInView = useInView(ref, { margin: "-50px 0px" }); + + const handleAnimationReset = useCallback(() => { + setResetKey((prevKey) => prevKey + 1); + setStep(Steps.Analyzing); + setIsAnimating(true); + }, []); + + const handleStepComplete = () => { + setStep(Steps.Finish); + setTimeout(() => { + setIsAnimating(false); + }, 5000); + }; + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === " ") { + event.preventDefault(); + + if (!isAnimating) { + handleAnimationReset?.(); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handleAnimationReset, isAnimating]); + + useEffect(() => { + if (isInView && !isAnimating && !hasPlayed) { + const timeout = setTimeout(() => { + handleAnimationReset(); + setHasPlayed(true); + }, 1500); + + return () => clearTimeout(timeout); + } + }, [isInView, isAnimating, hasPlayed, handleAnimationReset]); + + return ( +
+
+ + {!isAnimating && ( + + )} + + + + +
+ +
+ +
+ +
+
+
+
+ ); +} diff --git a/apps/frontend/components/templates/i18nPage/Preview/Play/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/Play/index.tsx new file mode 100644 index 000000000..af47fe7e5 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/Play/index.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { cn } from "@/utils"; +import { motion } from "framer-motion"; +import { PlayIcon } from "lucide-react"; + +interface PlayProps { + className?: string; + onClick: () => void; +} + +export function Play({ className, onClick }: PlayProps) { + return ( + +
+
+
+ +
+
+
+ +
+ space +
+
+ ); +} diff --git a/apps/frontend/components/templates/i18nPage/Preview/ScannerEffect/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/ScannerEffect/index.tsx new file mode 100644 index 000000000..81e3d871c --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/ScannerEffect/index.tsx @@ -0,0 +1,28 @@ +import { motion } from "framer-motion"; +import { memo } from "react"; + +const Scanner = ({ onComplete }: { onComplete?: () => void }) => { + return ( + +
+
+
+ + ); +}; + +export default memo(Scanner); diff --git a/apps/frontend/components/templates/i18nPage/Preview/ShinningLines/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/ShinningLines/index.tsx new file mode 100644 index 000000000..4916e115a --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/ShinningLines/index.tsx @@ -0,0 +1,57 @@ +"use client"; +import { motion, useInView } from "framer-motion"; +import type React from "react"; +import { useEffect, useRef, useState } from "react"; + +interface ShiningLinesProps { + numberOfLines?: number; +} + +const ShiningLines: React.FC = ({ numberOfLines = 6 }) => { + const ref = useRef(null); + const isInView = useInView(ref); + const lines = Array.from({ length: numberOfLines }); + const [shiningLineIndex, setShiningLineIndex] = useState(null); + + useEffect(() => { + if (!isInView) return; + + let previousIndex: number | null = null; + + const interval = setInterval(() => { + let nextIndex = Math.floor(Math.random() * numberOfLines); + while (nextIndex === previousIndex) { + nextIndex = Math.floor(Math.random() * numberOfLines); + } + setShiningLineIndex(nextIndex); + previousIndex = nextIndex; + }, 3500); + + return () => clearInterval(interval); + }, [isInView, numberOfLines]); + + return ( +
+ {lines.map((_, index) => ( +
+ {index === shiningLineIndex && ( + + )} +
+ ))} +
+ ); +}; + +export default ShiningLines; diff --git a/apps/frontend/components/templates/i18nPage/Preview/Spinner/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/Spinner/index.tsx new file mode 100644 index 000000000..2c20af83b --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/Spinner/index.tsx @@ -0,0 +1,25 @@ +const LoadingSpinner: React.FC = () => ( +
+ + Loading... +
+); + +export default LoadingSpinner; diff --git a/apps/frontend/components/templates/i18nPage/Preview/Step/TaskCard.tsx b/apps/frontend/components/templates/i18nPage/Preview/Step/TaskCard.tsx new file mode 100644 index 000000000..b48457526 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/Step/TaskCard.tsx @@ -0,0 +1,78 @@ +import Step from "@/components/templates/i18nPage/Preview/Step"; + +const TaskCard = ({ step }: { step: number }) => { + const stateMapping = [ + { + state: "Analyze", + tense: "Analyzing", + passed: "Analyzed", + description: () => ( + <> + Found 1,034 hardcoded strings, Saved 4 weeks of + engineering time. + + ), + }, + { + state: "Transform", + tense: "Transforming", + passed: "Transformed", + description: () => ( + <> + I18n-ized 1,034 strings, Saved 8 weeks of engineering + time. + + ), + }, + { + state: "Translate", + tense: "Translating", + passed: "Translated", + description: () => <>Work with one of our translation partners., + }, + ]; + + return ( +
+
+

Task #{step + 1}

+

+ {stateMapping[step]?.tense || "Finished!"} +

+

+ {stateMapping[step]?.description() || + "Work with one of our translation partners."} +

+
+ +
+
    + {stateMapping.map((item, index) => ( +
  • + +
  • + ))} +
+
+ + {/* Footer */} +
+ + Automated with Codemod AI + +
+
+ ); +}; + +export default TaskCard; diff --git a/apps/frontend/components/templates/i18nPage/Preview/Step/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/Step/index.tsx new file mode 100644 index 000000000..eb01cec0c --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/Step/index.tsx @@ -0,0 +1,124 @@ +import { cn } from "@/utils"; +import { motion } from "framer-motion"; +import type { ComponentProps } from "react"; + +export default function Step({ + step, + title, + currentStep, +}: { + step: number; + title: string; + currentStep: number; +}) { + const status = + currentStep === step + ? "active" + : currentStep < step + ? "inactive" + : "complete"; + + return ( + + + + + +
+ {status === "complete" ? ( + + ) : ( + + + + )} +
+
+
+ + {title} + +
+ ); +} + +function CheckIcon(props: ComponentProps<"svg">) { + return ( + + + + ); +} diff --git a/apps/frontend/components/templates/i18nPage/Preview/TextGenerateEffect/cursor.tsx b/apps/frontend/components/templates/i18nPage/Preview/TextGenerateEffect/cursor.tsx new file mode 100644 index 000000000..0577db449 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/TextGenerateEffect/cursor.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { cn } from "@/utils"; +import { + AnimatePresence, + animate, + motion, + useMotionValue, +} from "framer-motion"; +import React, { useState, useEffect } from "react"; + +export const LanguageSwitchAnimation = ({ + language, + onLanguageSwitch, + parentRef, +}: { + language: string; + onLanguageSwitch: () => void; + parentRef: React.RefObject; +}) => { + const x = useMotionValue(0); + const y = useMotionValue(0); + const [clicked, setClicked] = useState(false); + const [fadeOut, setFadeOut] = useState(false); + + const buttonRef = React.useRef(null); + + useEffect(() => { + if (parentRef.current) { + const parentRect = parentRef.current.getBoundingClientRect(); + const randomOffset = () => Math.random() * 150 + 100; + const centerX = + Math.random() < 0.5 + ? randomOffset() + : parentRect.width - randomOffset(); + const centerY = + Math.random() < 0.5 + ? randomOffset() + : parentRect.height - randomOffset(); + x.set(centerX); + y.set(centerY); + + const buttonElement = buttonRef.current; + if (buttonElement) { + const buttonRect = buttonElement.getBoundingClientRect(); + const parentOffsetX = buttonRect.left - parentRect.left; + const parentOffsetY = buttonRect.top - parentRect.top; + + const buttonBottomCenter = { + x: parentOffsetX + buttonRect.width / 2 - 13, + y: parentOffsetY + buttonRect.height / 2 + 13, + }; + + animate(x, buttonBottomCenter.x, { + duration: 6, + ease: "easeInOut", + }); + animate(y, buttonBottomCenter.y, { + duration: 6, + ease: "easeInOut", + onComplete: () => { + setClicked(true); + setTimeout(() => { + setClicked(false); + setFadeOut(true); + onLanguageSwitch(); + }, 300); + }, + }); + } + } + }, [parentRef]); + + return ( +
+ + {!fadeOut && ( + + )} + + + +
+ ); +}; + +export default function Cursor({ + onCompleted, +}: { + onCompleted: () => void; +}) { + const [language, setLanguage] = useState("🇺🇸"); + const parentRef = React.useRef(null); + + const handleLanguageSwitch = () => { + setLanguage((prev) => (prev === "🇺🇸" ? "🇪🇸" : "🇺🇸")); + onCompleted(); + }; + + return ( +
+ +
+ ); +} diff --git a/apps/frontend/components/templates/i18nPage/Preview/TextGenerateEffect/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/TextGenerateEffect/index.tsx new file mode 100644 index 000000000..8d8246f23 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/TextGenerateEffect/index.tsx @@ -0,0 +1,82 @@ +"use client"; +import { useAnimate } from "framer-motion"; +import { useEffect } from "react"; +import React from "react"; + +export const TextGenerateEffect = ({ + baseWords, + translatedWords, + as: Wrapper = "p", // Default wrapper component is "p" + className, + duration = 0.5, + delay = 0, + triggered = false, // Trigger animation as a prop + detected = false, + borderColor = "#ff6347", // Default color for the dashed border +}: { + baseWords: string; + translatedWords: string; + as?: React.ElementType; // Component to use as wrapper + className?: string; + duration?: number; + delay?: number; // Time to delay the animation start + triggered?: boolean; // Trigger for animation + detected?: boolean; // Detect for animation + borderColor?: string; // Dashed border color +}) => { + const [scope, animate] = useAnimate(); + + useEffect(() => { + const wrapper = scope.current; + + if (wrapper && detected) { + // Step 1: Detect the words by showing the outline immediately + animate( + wrapper, + { outlineColor: borderColor }, + { duration: 0.5 }, // 500ms for the dashed border fade-in + ); + } + }, [detected]); // Run once on component mount + + useEffect(() => { + if (detected && triggered) { + const wrapper = scope.current; + + if (wrapper) { + // Step 2: Proceed with the animation sequence after detection and trigger + setTimeout(() => { + // Step 3: Fade out the dashed border and blur the text + animate( + wrapper, + { outline: "none", opacity: 0, filter: "blur(10px)" }, + { duration: duration }, + ).then(() => { + // Step 4: Replace content with translated words + wrapper.textContent = translatedWords; + + // Step 5: Fade in the new words + animate( + wrapper, + { opacity: 1, filter: "blur(0px)" }, + { duration: duration }, + ); + }); + }, delay * 1000); // Delay before the fade-out and blur animation starts + } + } + }, [triggered, detected]); // React to changes in `triggered` and `detected` + + return React.createElement( + Wrapper, + { + ref: scope, + className: `inline-block relative outline transition-opacity duration-500 ${className}`, + style: { + opacity: 1, // Fully visible initially + filter: "blur(0px)", // No blur initially + }, + }, + baseWords, + ); +}; diff --git a/apps/frontend/components/templates/i18nPage/Preview/Timeline/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/Timeline/index.tsx new file mode 100644 index 000000000..39f670158 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/Timeline/index.tsx @@ -0,0 +1,93 @@ +import { motion } from "framer-motion"; +import CircularProgress from "../CircularProgress"; + +const childVariants = { + initial: { opacity: 0, x: -20 }, + animate: { + opacity: 1, + x: 0, + transition: { + staggerChildren: 0.1, + delay: 0.25, + }, + }, + exit: { opacity: 0, x: 20 }, +}; + +const stateMapping = [ + { + label: "Analyzing", + percentage: 40, + description: + "Found 1,034 hardcoded strings, Saved 4 weeks of engineering time.", + }, + { + label: "Transforming", + percentage: 70, + description: "I18n-ized 1,034 strings, Saved 8 weeks of engineering time.", + }, + { + label: "Translating", + percentage: 90, + description: "Work with one of our translation partners.", + }, + { + label: "Ready", + percentage: 100, + description: + "Your project is fully internationalized and ready to translate.", + }, +]; + +export const Timeline = ({ + step, + isAnimating, +}: { step: number; isAnimating: boolean }) => { + const currentStep = + !isAnimating && step === 3 + ? 3 + : Math.min(step > 1 ? step - 1 : 0, stateMapping.length - 1); + const isAI = step < 3; + + return ( +
+ {currentStep < stateMapping.length && ( +
+ + + + + {stateMapping[currentStep]?.label} + + + {stateMapping[currentStep]?.description} + + + {isAI && ( + + Automated by Codemod AI + + )} + +
+ )} +
+ ); +}; diff --git a/apps/frontend/components/templates/i18nPage/Preview/hooks.ts b/apps/frontend/components/templates/i18nPage/Preview/hooks.ts new file mode 100644 index 000000000..114184492 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/hooks.ts @@ -0,0 +1,92 @@ +import { type HighlightedCode, highlight } from "codehike/code"; +import { useCallback, useLayoutEffect, useState } from "react"; +import { + CODE_STEP_1, + CODE_STEP_2, + CODE_STEP_3, + JSON_STEP_1, + JSON_STEP_2, +} from "./CodeSwitcher/constants"; + +// Custom hook for theme detection (moved to a shared hook for reusability) +function useThemeDetector(): "dark" | "light" | null { + const [theme, setTheme] = useState<"dark" | "light" | null>(() => { + if (typeof window !== "undefined") { + const storedTheme = localStorage.getItem("theme"); + if (storedTheme === "dark" || storedTheme === "light") { + return storedTheme; + } + } + return null; + }); + + useLayoutEffect(() => { + if (typeof document !== "undefined") { + const isLight = document.documentElement.classList.contains("light"); + setTheme(isLight ? "light" : "dark"); + + const observer = new MutationObserver(() => { + const isLight = document.documentElement.classList.contains("light"); + setTheme(isLight ? "light" : "dark"); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + } + }, []); + + return theme; +} + +// Custom Hook: Fetch Highlights +const useHighlights = () => { + const [infos, setInfos] = useState([]); + const [jsonInfos, setJsonInfos] = useState(null); + const [loading, setLoading] = useState(true); + const theme = useThemeDetector(); + + const fetchHighlights = useCallback(async () => { + if (!theme) return; + + const tsCodeBlocks = [CODE_STEP_1, CODE_STEP_2, CODE_STEP_3]; + const jsonCodeBlocks = [JSON_STEP_1, JSON_STEP_2]; + const palette = theme === "dark" ? "github-dark" : "github-light"; + + try { + const tsResults = await Promise.all( + tsCodeBlocks.map((code) => + highlight({ lang: "tsx", value: code, meta: "" }, palette), + ), + ); + + const jsonResults = await Promise.all( + jsonCodeBlocks.map((code) => + highlight({ lang: "json", value: code, meta: "" }, palette), + ), + ); + + setInfos(tsResults); + setJsonInfos(jsonResults); + } catch (error) { + console.error("Error fetching highlights:", error); + } finally { + setLoading(false); + } + }, [theme]); + + useLayoutEffect(() => { + const timer = setTimeout(() => { + fetchHighlights(); + }, 300); // Adjust debounce delay as necessary + + return () => clearTimeout(timer); + }, [fetchHighlights]); + + return { infos, jsonInfos, loading }; +}; + +export { useThemeDetector, useHighlights }; diff --git a/apps/frontend/components/templates/i18nPage/Preview/index.tsx b/apps/frontend/components/templates/i18nPage/Preview/index.tsx new file mode 100644 index 000000000..3344f2061 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/index.tsx @@ -0,0 +1,158 @@ +import { TextGenerateEffect } from "@/components/templates/i18nPage/Preview/TextGenerateEffect"; +import Cursor from "@/components/templates/i18nPage/Preview/TextGenerateEffect/cursor"; +import { + StepState, + Steps, +} from "@/components/templates/i18nPage/Preview/types"; +import { AnimatePresence } from "framer-motion"; +import type React from "react"; +import { memo } from "react"; + +interface PreviewProps { + step: Steps; + isAnimating: boolean; + onStepComplete: () => void; +} + +const getStepState = (step: Steps, isAnimating: boolean): StepState => { + if (step >= Steps.Finish) return StepState.Finished; + if (step >= Steps.Translating && isAnimating) return StepState.Animated; + if (step >= Steps.Transforming && isAnimating) + return StepState.AnimatedCursor; + if (step >= Steps.Analyzing && isAnimating) return StepState.Detected; + return StepState.None; +}; + +const COLORS = [ + "#ef4444", + "#22c55e", + "#3b82f6", + "#eab308", + "#a855f7", + "#f97316", + "#84cc16", + "#06b6d4", + "#f43f5e", +]; + +const translations: Record = { + "Create an account": "Crea una cuenta", + "Enter your email below to create your account": + "Ingresa tu correo electrónico abajo para crear tu cuenta", + "Enter your email...": "Introduce tu correo electrónico...", + "Sign In with Email": "Inicia sesión con tu correo", + "Or continue with": "O continúa con", + GitHub: "GitHub", + "Don't have an account?": "¿No tienes una cuenta?", + "Sign up": "Regístrate", +}; + +const Preview: React.FC = ({ + step, + isAnimating, + onStepComplete, +}) => { + const stepState = getStepState(step, isAnimating); + + const renderTextEffect = ( + baseWords: string, + colorIndex: number, + extraProps = {}, + ) => ( + StepState.Animated} + detected={stepState >= StepState.AnimatedCursor} + borderColor={COLORS[colorIndex]} + {...extraProps} + /> + ); + + return ( +
+ + {stepState >= StepState.Animated && ( + + )} + +
+
+ {/* Header Section */} + {renderTextEffect("Create an account", 0, { + as: "h2", + className: "mb-2 text-center m-heading", + })} + {renderTextEffect( + "Enter your email below to create your account", + 1, + { + as: "div", + className: "mb-4 text-center text-sm opacity-60", + }, + )} + + {/* Form Section */} +
+
+ {renderTextEffect("Enter your email...", 2, { + as: "div", + className: "opacity-35", + })} +
+
+ {renderTextEffect("Sign In with Email", 3, { + as: "div", + })} +
+
+ + {/* Divider Section */} +
+
+ + {renderTextEffect("Or continue with", 4, { + as: "div", + duration: 1, + })} + +
+
+ + {/* GitHub Button */} + + + {/* Footer Section */} +
+ {renderTextEffect("Don't have an account?", 6, { + as: "div", + })} + + {renderTextEffect("Sign up", 7, { as: "div" })} + +
+
+
+
+ ); +}; + +export default memo(Preview); diff --git a/apps/frontend/components/templates/i18nPage/Preview/types.ts b/apps/frontend/components/templates/i18nPage/Preview/types.ts new file mode 100644 index 000000000..4335ac130 --- /dev/null +++ b/apps/frontend/components/templates/i18nPage/Preview/types.ts @@ -0,0 +1,14 @@ +export enum Steps { + Analyzing = 0, + Transforming = 1, + Translating = 2, + Finish = 3, +} + +export enum StepState { + None = 0, + Detected = 1, + AnimatedCursor = 2, + Animated = 3, + Finished = 4, +} diff --git a/apps/frontend/fonts/index.ts b/apps/frontend/fonts/index.ts index 77518a29d..f04bb5fb8 100644 --- a/apps/frontend/fonts/index.ts +++ b/apps/frontend/fonts/index.ts @@ -1,31 +1,14 @@ import { cx } from "cva"; -import localFont from "next/font/local"; +import { GeistMono } from "geist/font/mono"; +import { Plus_Jakarta_Sans } from "next/font/google"; -const satoshiRegular = localFont({ - src: "./Satoshi-Regular.woff2", - display: "swap", - variable: "--satoshi-regular", - weight: "400", +export const SansFont = Plus_Jakarta_Sans({ + subsets: ["latin"], + weight: ["400", "500", "700"], + variable: "--font-sans", }); +export const geistMono = GeistMono; -const satoshiMedium = localFont({ - src: "./Satoshi-Medium.woff2", - display: "swap", - variable: "--satoshi-medium", - weight: "500", -}); - -const satoshiBold = localFont({ - src: "./Satoshi-Bold.woff2", - display: "swap", - variable: "--satoshi-bold", - weight: "700", -}); - -const globalFontsVariables = cx( - satoshiRegular.variable, - satoshiMedium.variable, - satoshiBold.variable, -); +const globalFontsVariables = cx(SansFont.className, geistMono.variable); export default globalFontsVariables; diff --git a/apps/frontend/package.json b/apps/frontend/package.json index deb9128dc..30c2ff891 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -84,6 +84,8 @@ "class-variance-authority": "catalog:", "classnames": "2.5.1", "clsx": "catalog:", + "cobe": "^0.6.3", + "codehike": "^1.0.4", "constants-browserify": "catalog:", "cva": "catalog:", "esbuild-wasm": "^0.23.0", @@ -101,6 +103,7 @@ "lucide-react": "catalog:", "match-sorter": "catalog:", "monaco-editor": "^0.36.1", + "motion": "^11.18.0", "next": "catalog:", "next-sanity": "catalog:", "node-fetch": "catalog:", diff --git a/apps/frontend/styles/globals.css b/apps/frontend/styles/globals.css index 3034d26c7..3b1899ab8 100644 --- a/apps/frontend/styles/globals.css +++ b/apps/frontend/styles/globals.css @@ -56,7 +56,7 @@ animation-play-state: paused; } .xl-heading { - @apply font-bold text-xlHeadingMobile lg:text-xlHeading; + @apply text-xlHeadingMobile lg:text-xlHeading; } .l-heading { @@ -134,7 +134,7 @@ html { body { overflow-x: hidden; - font-family: var(--satoshi-regular); + word-spacing: 1px; @apply selection:bg-accent selection:text-primary-light; } @@ -386,3 +386,23 @@ h6[class*="heading"] a { transform: translateX(3px); } } + +.scrollbar::-webkit-scrollbar { + @apply w-1; +} +.scrollbar::-webkit-scrollbar, .scrollbar::-webkit-scrollbar-thumb { + @apply overflow-visible rounded; +} +.scrollbar::-webkit-scrollbar-thumb { + @apply bg-slate-500/20; +} + + +.outline { + outline-width: 1px; + outline-style: dashed; + outline-color: transparent; + outline-offset: 2px; + border-radius: 3px; +} + diff --git a/apps/frontend/tailwind.config.ts b/apps/frontend/tailwind.config.ts index 11f09925a..bf7d2ed7b 100644 --- a/apps/frontend/tailwind.config.ts +++ b/apps/frontend/tailwind.config.ts @@ -214,10 +214,10 @@ export default { extend: { colors: colorPalette, fontFamily: { - regular: "var(--satoshi-regular)", - medium: "var(--satoshi-medium)", - bold: "var(--satoshi-bold)", - mono: ["var(--inconsolata)"], + regular: "var(--font-sans)", + medium: "var(--font-sans)", + bold: "var(--font-sans)", + mono: ["var(--font-geist-mono)"], }, fontSize: { xlHeading: [ @@ -257,7 +257,7 @@ export default { { fontWeight: "700", lineHeight: "36px", - letterSpacing: "-1.28px", + letterSpacing: "-0.96px", }, ], mdHeadingMobile: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bebb51ad6..eebe5eeb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1447,6 +1447,12 @@ importers: clsx: specifier: 'catalog:' version: 2.1.1 + cobe: + specifier: ^0.6.3 + version: 0.6.3 + codehike: + specifier: ^1.0.4 + version: 1.0.4 constants-browserify: specifier: 'catalog:' version: 1.0.0 @@ -1498,6 +1504,9 @@ importers: monaco-editor: specifier: ^0.36.1 version: 0.36.1 + motion: + specifier: ^11.18.0 + version: 11.18.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) next: specifier: 'catalog:' version: 14.2.4(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -3870,6 +3879,9 @@ packages: resolution: {integrity: sha512-SPBRHdP+ZggeudncbSj8KLOZ5PdVmtgZhL+tXremVJrETi9WHpf1UOYgnIHl+clp6GZC2IQM4tq57RSrHf0Oew==} engines: {node: '>=18.17.0'} + '@code-hike/lighter@1.0.1': + resolution: {integrity: sha512-mccvcsk5UTScRrE02oBz1/qzckyhD8YE3VQlQv++2bSVVZgNuCUX8MpokSCi5OmfRAAxbj6kmNiqq1Um8eXPrw==} + '@codemirror/autocomplete@6.16.3': resolution: {integrity: sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA==} peerDependencies: @@ -6566,6 +6578,7 @@ packages: '@sanity/block-tools@3.47.1': resolution: {integrity: sha512-R95TWfdxggaXmSs42MjuHkbJOzme/HHr1vU3bGd/aPe7WmernevAzfaHToYMB4eyJEzy0s0WKlD0/evGIb9WKw==} + deprecated: Renamed - use `@portabletext/block-tools` instead. `@sanity/block-tools` will no longer receive updates. '@sanity/cli@3.47.1': resolution: {integrity: sha512-y2CJ/pojoHrbV1FqPPyRmEPcHc0OTsBRxgCMVnFieF/B0Gl3EF8i7tQcjb4xeHqymoNlyQI1zyZvPLTJF2nSPg==} @@ -8042,6 +8055,7 @@ packages: '@vscode/webview-ui-toolkit@1.4.0': resolution: {integrity: sha512-modXVHQkZLsxgmd5yoP3ptRC/G8NBDD+ob+ngPiWNQdlrH6H1xR/qgOBD85bfU3BhOB5sZzFWBwwhp9/SfoHww==} + deprecated: This package has been deprecated, https://github.com/microsoft/vscode-webview-ui-toolkit/issues/561 peerDependencies: react: '>=16.9.0' @@ -8346,6 +8360,9 @@ packages: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} + ansi-sequence-parser@1.1.1: + resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} + ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -9017,6 +9034,9 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + cobe@0.6.3: + resolution: {integrity: sha512-WHr7X4o1ym94GZ96h7b1pNemZJacbOzd02dZtnVwuC4oWBaLg96PBmp2rIS1SAhUDhhC/QyS9WEqkpZIs/ZBTg==} + cockatiel@3.1.3: resolution: {integrity: sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==} engines: {node: '>=16'} @@ -9030,6 +9050,9 @@ packages: code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + codehike@1.0.4: + resolution: {integrity: sha512-mG/YJiK5J9tFHp/2seoliZpT4uxzjcbwDWXWXcYPRQKgQa4iRtwzVsOp/L4FnEX1J9LceZjCe3+ztSiktrcV1w==} + codemirror@6.0.1: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} @@ -10441,6 +10464,20 @@ packages: react-dom: optional: true + framer-motion@11.18.0: + resolution: {integrity: sha512-Vmjl5Al7XqKHzDFnVqzi1H9hzn5w4eN/bdqXTymVpU2UuMQuz9w6UPdsL9dFBeH7loBlnu4qcEXME+nvbkcIOw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + framer-motion@11.2.11: resolution: {integrity: sha512-n+ozoEzgJu/2h9NoQMokF+CwNqIRVyuRC4RwMPwklfrrTjbVV32k9uBIgqYAwn7Jfpt5LuDVCtT57MWz1FbaLw==} peerDependencies: @@ -12451,6 +12488,26 @@ packages: peerDependencies: fp-ts: ^2.5.0 + motion-dom@11.16.4: + resolution: {integrity: sha512-2wuCie206pCiP2K23uvwJeci4pMFfyQKpWI0Vy6HrCTDzDCer4TsYtT7IVnuGbDeoIV37UuZiUr6SZMHEc1Vww==} + + motion-utils@11.16.0: + resolution: {integrity: sha512-ngdWPjg31rD4WGXFi0eZ00DQQqKKu04QExyv/ymlC+3k+WIgYVFbt6gS5JsFPbJODTF/r8XiE/X+SsoT9c0ocw==} + + motion@11.18.0: + resolution: {integrity: sha512-uJ4zNXh/4K9C5wftxHKlXLHC0Rc9dHSHPyO1P6T9XE2bTn2z8C2lOZX/M8vAmFp0gtJTJ3aYkv44lTtJSfv6+A==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -13202,6 +13259,9 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + phenomenon@1.6.0: + resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==} + picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -16595,13 +16655,13 @@ snapshots: dependencies: '@aws-crypto/util': 5.2.0 '@aws-sdk/types': 3.598.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 '@aws-sdk/types': 3.598.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: @@ -16610,7 +16670,7 @@ snapshots: '@aws-sdk/types': 3.598.0 '@aws-sdk/util-locate-window': 3.568.0 '@smithy/util-utf8': 2.3.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': dependencies: @@ -16620,23 +16680,23 @@ snapshots: '@aws-sdk/types': 3.598.0 '@aws-sdk/util-locate-window': 3.568.0 '@smithy/util-utf8': 2.3.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 '@aws-sdk/types': 3.598.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@aws-crypto/util@5.2.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/util-utf8': 2.3.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/client-s3@3.600.0': dependencies: @@ -16742,7 +16802,7 @@ snapshots: '@smithy/util-middleware': 3.0.2 '@smithy/util-retry': 3.0.2 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/client-sts' - aws-crt @@ -16831,7 +16891,7 @@ snapshots: '@smithy/util-middleware': 3.0.2 '@smithy/util-retry': 3.0.2 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -16843,14 +16903,14 @@ snapshots: '@smithy/smithy-client': 3.1.4 '@smithy/types': 3.2.0 fast-xml-parser: 4.2.5 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/credential-provider-env@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/property-provider': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/credential-provider-http@3.598.0': dependencies: @@ -16862,7 +16922,7 @@ snapshots: '@smithy/smithy-client': 3.1.4 '@smithy/types': 3.2.0 '@smithy/util-stream': 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/credential-provider-ini@3.598.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0)': dependencies: @@ -16877,7 +16937,7 @@ snapshots: '@smithy/property-provider': 3.1.2 '@smithy/shared-ini-file-loader': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - aws-crt @@ -16895,7 +16955,7 @@ snapshots: '@smithy/property-provider': 3.1.2 '@smithy/shared-ini-file-loader': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - '@aws-sdk/client-sts' @@ -16907,7 +16967,7 @@ snapshots: '@smithy/property-provider': 3.1.2 '@smithy/shared-ini-file-loader': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/credential-provider-sso@3.598.0(@aws-sdk/client-sso-oidc@3.600.0)': dependencies: @@ -16917,7 +16977,7 @@ snapshots: '@smithy/property-provider': 3.1.2 '@smithy/shared-ini-file-loader': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - aws-crt @@ -16928,7 +16988,7 @@ snapshots: '@aws-sdk/types': 3.598.0 '@smithy/property-provider': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-bucket-endpoint@3.598.0': dependencies: @@ -16938,14 +16998,14 @@ snapshots: '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 '@smithy/util-config-provider': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-expect-continue@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-flexible-checksums@3.598.0': dependencies: @@ -16956,33 +17016,33 @@ snapshots: '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-host-header@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-location-constraint@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-logger@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-recursion-detection@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-sdk-s3@3.598.0': dependencies: @@ -16994,7 +17054,7 @@ snapshots: '@smithy/smithy-client': 3.1.4 '@smithy/types': 3.2.0 '@smithy/util-config-provider': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-signing@3.598.0': dependencies: @@ -17004,13 +17064,13 @@ snapshots: '@smithy/signature-v4': 3.1.1 '@smithy/types': 3.2.0 '@smithy/util-middleware': 3.0.2 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-ssec@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/middleware-user-agent@3.598.0': dependencies: @@ -17018,7 +17078,7 @@ snapshots: '@aws-sdk/util-endpoints': 3.598.0 '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/region-config-resolver@3.598.0': dependencies: @@ -17027,7 +17087,7 @@ snapshots: '@smithy/types': 3.2.0 '@smithy/util-config-provider': 3.0.0 '@smithy/util-middleware': 3.0.2 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/s3-request-presigner@3.600.0': dependencies: @@ -17047,7 +17107,7 @@ snapshots: '@smithy/protocol-http': 4.0.2 '@smithy/signature-v4': 3.1.1 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/token-providers@3.598.0(@aws-sdk/client-sso-oidc@3.600.0)': dependencies: @@ -17061,62 +17121,62 @@ snapshots: '@aws-sdk/types@3.598.0': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/util-arn-parser@3.568.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/util-endpoints@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/types': 3.2.0 '@smithy/util-endpoints': 2.0.3 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/util-format-url@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/querystring-builder': 3.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/util-locate-window@3.568.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/util-user-agent-browser@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/types': 3.2.0 bowser: 2.11.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/util-user-agent-node@3.598.0': dependencies: '@aws-sdk/types': 3.598.0 '@smithy/node-config-provider': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@aws-sdk/xml-builder@3.598.0': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@azure/abort-controller@1.1.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@azure/abort-controller@2.1.2': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@azure/core-auth@1.7.2': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-util': 1.9.0 - tslib: 2.6.3 + tslib: 2.8.1 '@azure/core-client@1.9.2': dependencies: @@ -17126,7 +17186,7 @@ snapshots: '@azure/core-tracing': 1.1.2 '@azure/core-util': 1.9.0 '@azure/logger': 1.1.2 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -17140,7 +17200,7 @@ snapshots: form-data: 4.0.1 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 - tslib: 2.6.3 + tslib: 2.8.1 uuid: 8.3.2 transitivePeerDependencies: - supports-color @@ -17154,23 +17214,23 @@ snapshots: '@azure/logger': 1.1.2 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.4 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - supports-color '@azure/core-tracing@1.1.2': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@azure/core-util@1.2.0': dependencies: '@azure/abort-controller': 1.1.0 - tslib: 2.6.3 + tslib: 2.8.1 '@azure/core-util@1.9.0': dependencies: '@azure/abort-controller': 2.1.2 - tslib: 2.6.3 + tslib: 2.8.1 '@azure/identity@4.3.0': dependencies: @@ -17187,13 +17247,13 @@ snapshots: jws: 4.0.0 open: 8.4.2 stoppable: 1.1.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - supports-color '@azure/logger@1.1.2': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@azure/msal-browser@3.17.0': dependencies: @@ -17214,7 +17274,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.41.2(@opentelemetry/api@1.9.0) - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -18368,6 +18428,10 @@ snapshots: dependencies: csstype: 3.1.1 + '@code-hike/lighter@1.0.1': + dependencies: + ansi-sequence-parser: 1.1.1 + '@codemirror/autocomplete@6.16.3(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.28.2)(@lezer/common@1.2.1)': dependencies: '@codemirror/language': 6.10.2 @@ -21548,16 +21612,16 @@ snapshots: '@smithy/abort-controller@3.1.0': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/chunked-blob-reader-native@3.0.0': dependencies: '@smithy/util-base64': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/chunked-blob-reader@3.0.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/config-resolver@3.0.3': dependencies: @@ -21565,7 +21629,7 @@ snapshots: '@smithy/types': 3.2.0 '@smithy/util-config-provider': 3.0.0 '@smithy/util-middleware': 3.0.2 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/core@2.2.3': dependencies: @@ -21576,7 +21640,7 @@ snapshots: '@smithy/smithy-client': 3.1.4 '@smithy/types': 3.2.0 '@smithy/util-middleware': 3.0.2 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/credential-provider-imds@3.1.2': dependencies: @@ -21584,7 +21648,7 @@ snapshots: '@smithy/property-provider': 3.1.2 '@smithy/types': 3.2.0 '@smithy/url-parser': 3.0.2 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/eventstream-codec@3.1.1': dependencies: @@ -21597,24 +21661,24 @@ snapshots: dependencies: '@smithy/eventstream-serde-universal': 3.0.3 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/eventstream-serde-config-resolver@3.0.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/eventstream-serde-node@3.0.3': dependencies: '@smithy/eventstream-serde-universal': 3.0.3 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/eventstream-serde-universal@3.0.3': dependencies: '@smithy/eventstream-codec': 3.1.1 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/fetch-http-handler@3.1.0': dependencies: @@ -21622,32 +21686,32 @@ snapshots: '@smithy/querystring-builder': 3.0.2 '@smithy/types': 3.2.0 '@smithy/util-base64': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/hash-blob-browser@3.1.1': dependencies: '@smithy/chunked-blob-reader': 3.0.0 '@smithy/chunked-blob-reader-native': 3.0.0 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/hash-node@3.0.2': dependencies: '@smithy/types': 3.2.0 '@smithy/util-buffer-from': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/hash-stream-node@3.1.1': dependencies: '@smithy/types': 3.2.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/invalid-dependency@3.0.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': dependencies: @@ -21655,19 +21719,19 @@ snapshots: '@smithy/is-array-buffer@3.0.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/md5-js@3.0.2': dependencies: '@smithy/types': 3.2.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/middleware-content-length@3.0.2': dependencies: '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/middleware-endpoint@3.0.3': dependencies: @@ -21677,7 +21741,7 @@ snapshots: '@smithy/types': 3.2.0 '@smithy/url-parser': 3.0.2 '@smithy/util-middleware': 3.0.2 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/middleware-retry@3.0.6': dependencies: @@ -21688,25 +21752,25 @@ snapshots: '@smithy/types': 3.2.0 '@smithy/util-middleware': 3.0.2 '@smithy/util-retry': 3.0.2 - tslib: 2.6.3 + tslib: 2.8.1 uuid: 9.0.1 '@smithy/middleware-serde@3.0.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/middleware-stack@3.0.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/node-config-provider@3.1.2': dependencies: '@smithy/property-provider': 3.1.2 '@smithy/shared-ini-file-loader': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/node-http-handler@3.1.0': dependencies: @@ -21714,28 +21778,28 @@ snapshots: '@smithy/protocol-http': 4.0.2 '@smithy/querystring-builder': 3.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/property-provider@3.1.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/protocol-http@4.0.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/querystring-builder@3.0.2': dependencies: '@smithy/types': 3.2.0 '@smithy/util-uri-escape': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/querystring-parser@3.0.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/service-error-classification@3.0.2': dependencies: @@ -21744,7 +21808,7 @@ snapshots: '@smithy/shared-ini-file-loader@3.1.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/signature-v4@3.1.1': dependencies: @@ -21754,7 +21818,7 @@ snapshots: '@smithy/util-middleware': 3.0.2 '@smithy/util-uri-escape': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/smithy-client@3.1.4': dependencies: @@ -21763,31 +21827,31 @@ snapshots: '@smithy/protocol-http': 4.0.2 '@smithy/types': 3.2.0 '@smithy/util-stream': 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/types@3.2.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/url-parser@3.0.2': dependencies: '@smithy/querystring-parser': 3.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-base64@3.0.0': dependencies: '@smithy/util-buffer-from': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-body-length-browser@3.0.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-body-length-node@3.0.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-buffer-from@2.2.0': dependencies: @@ -21797,11 +21861,11 @@ snapshots: '@smithy/util-buffer-from@3.0.0': dependencies: '@smithy/is-array-buffer': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-config-provider@3.0.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-defaults-mode-browser@3.0.6': dependencies: @@ -21809,7 +21873,7 @@ snapshots: '@smithy/smithy-client': 3.1.4 '@smithy/types': 3.2.0 bowser: 2.11.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-defaults-mode-node@3.0.6': dependencies: @@ -21819,28 +21883,28 @@ snapshots: '@smithy/property-provider': 3.1.2 '@smithy/smithy-client': 3.1.4 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-endpoints@2.0.3': dependencies: '@smithy/node-config-provider': 3.1.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-hex-encoding@3.0.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-middleware@3.0.2': dependencies: '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-retry@3.0.2': dependencies: '@smithy/service-error-classification': 3.0.2 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-stream@3.0.4': dependencies: @@ -21851,7 +21915,7 @@ snapshots: '@smithy/util-buffer-from': 3.0.0 '@smithy/util-hex-encoding': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-uri-escape@3.0.0': dependencies: @@ -21860,18 +21924,18 @@ snapshots: '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-utf8@3.0.0': dependencies: '@smithy/util-buffer-from': 3.0.0 - tslib: 2.6.3 + tslib: 2.8.1 '@smithy/util-waiter@3.1.1': dependencies: '@smithy/abort-controller': 3.1.0 '@smithy/types': 3.2.0 - tslib: 2.6.3 + tslib: 2.8.1 '@socket.io/component-emitter@3.1.2': {} @@ -23438,6 +23502,8 @@ snapshots: ansi-regex@6.0.1: {} + ansi-sequence-parser@1.1.1: {} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 @@ -23635,7 +23701,7 @@ snapshots: ast-types@0.16.1: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 ast-types@0.9.14: {} @@ -24181,6 +24247,10 @@ snapshots: cluster-key-slot@1.1.2: {} + cobe@0.6.3: + dependencies: + phenomenon: 1.6.0 + cockatiel@3.1.3: {} code-block-writer@12.0.0: {} @@ -24195,6 +24265,16 @@ snapshots: estree-walker: 3.0.3 periscopic: 3.1.0 + codehike@1.0.4: + dependencies: + '@code-hike/lighter': 1.0.1 + diff: 5.2.0 + estree-util-visit: 2.0.0 + mdast-util-mdx-jsx: 3.1.2 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - supports-color + codemirror@6.0.1(@lezer/common@1.2.1): dependencies: '@codemirror/autocomplete': 6.16.3(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.28.2)(@lezer/common@1.2.1) @@ -25993,7 +26073,7 @@ snapshots: framer-motion@10.18.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - tslib: 2.6.3 + tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 react: 18.2.0 @@ -26007,6 +26087,16 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + framer-motion@11.18.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + motion-dom: 11.16.4 + motion-utils: 11.16.0 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.2.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + framer-motion@11.2.11(@emotion/is-prop-valid@1.2.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: tslib: 2.6.3 @@ -28497,6 +28587,21 @@ snapshots: dependencies: fp-ts: 2.16.6 + motion-dom@11.16.4: + dependencies: + motion-utils: 11.16.0 + + motion-utils@11.16.0: {} + + motion@11.18.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + framer-motion: 11.18.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.2.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + mri@1.2.0: {} ms@2.0.0: {} @@ -29223,6 +29328,8 @@ snapshots: dependencies: split2: 4.2.0 + phenomenon@1.6.0: {} + picocolors@1.0.1: {} picocolors@1.1.1: {} @@ -29889,7 +29996,7 @@ snapshots: dependencies: react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.55)(react@18.2.0) - tslib: 2.6.3 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.2.55 @@ -29964,7 +30071,7 @@ snapshots: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 - tslib: 2.6.3 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.2.55 @@ -32274,7 +32381,7 @@ snapshots: use-callback-ref@1.3.2(@types/react@18.2.55)(react@18.2.0): dependencies: react: 18.2.0 - tslib: 2.6.3 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.2.55 @@ -32327,7 +32434,7 @@ snapshots: dependencies: detect-node-es: 1.1.0 react: 18.2.0 - tslib: 2.6.3 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.2.55