commit ebd8286e753853e248912eb4d997a480ff5e60eb Author: juan Date: Sat Sep 13 17:45:04 2025 +0200 first commit diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..b7bdc3f --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +EMAIL_SENDER="p3n4lv3r@gmail.com" \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..b7bdc3f --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +EMAIL_SENDER="p3n4lv3r@gmail.com" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be22d1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +node_modules +package-lock.json +yarn.lock +dist +.netlify + +.DS_Store +.cache +.env +.vercel +.output +.vinxi + +/build/ +/api/ +/server/build +/public/build +.vinxi +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2be5eaa --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..00b5278 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..90cba4a --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Welcome to TanStack.com! + +This site is built with TanStack Router! + +- [TanStack Router Docs](https://tanstack.com/router) + +It's deployed automagically with Netlify! + +- [Netlify](https://netlify.com/) + +## Development + +From your terminal: + +```sh +pnpm install +pnpm dev +``` + +This starts your app in development mode, rebuilding assets on file changes. + +## Editing and previewing the docs of TanStack projects locally + +The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app. +In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system. + +Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally : + +1. Create a new directory called `tanstack`. + +```sh +mkdir tanstack +``` + +2. Enter the directory and clone this repo and the repo of the project there. + +```sh +cd tanstack +git clone git@github.com:TanStack/tanstack.com.git +git clone git@github.com:TanStack/form.git +``` + +> [!NOTE] +> Your `tanstack` directory should look like this: +> +> ``` +> tanstack/ +> | +> +-- form/ +> | +> +-- tanstack.com/ +> ``` + +> [!WARNING] +> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found. + +3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: + +```sh +cd tanstack.com +pnpm i +# The app will run on https://localhost:3000 by default +pnpm dev +``` + +4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`. + +> [!NOTE] +> The updated pages need to be manually reloaded in the browser. + +> [!WARNING] +> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page! diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 0000000..5fbb7e0 --- /dev/null +++ b/app.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "@tanstack/react-start/config"; +import tsConfigPaths from "vite-tsconfig-paths"; +import { sitemap } from "./src/utils/sitemap"; +import { generateSitemap } from "tanstack-router-sitemap"; + + +export default defineConfig({ + server: { + preset: "netlify", + prerender: { + routes: ["/", "/seguros", "/formulario"], + crawlLinks: true, + }, + }, + tsr: { + appDirectory: "src", + }, + vite: { + plugins: [ + tsConfigPaths({ + projects: ["./tsconfig.json"], + }), + generateSitemap(sitemap) + ], + }, +}); diff --git a/jsrepo.json b/jsrepo.json new file mode 100644 index 0000000..ac4a9be --- /dev/null +++ b/jsrepo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://unpkg.com/jsrepo@1.47.1/schemas/project-config.json", + "repos": ["https://reactbits.dev/ts/tailwind"], + "includeTests": false, + "watermark": true, + "formatter": "prettier", + "configFiles": {}, + "paths": { + "*": "./src/blocks", + "TextAnimations": "./src/TextAnimations" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c3f387c --- /dev/null +++ b/package.json @@ -0,0 +1,89 @@ +{ + "name": "tanstack-start-example-basic", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@heroui/accordion": "^2.2.13", + "@heroui/alert": "^2.2.16", + "@heroui/autocomplete": "^2.3.17", + "@heroui/avatar": "^2.2.12", + "@heroui/badge": "^2.2.10", + "@heroui/breadcrumbs": "^2.2.12", + "@heroui/button": "^2.2.16", + "@heroui/calendar": "^2.2.16", + "@heroui/card": "^2.2.15", + "@heroui/checkbox": "^2.3.15", + "@heroui/chip": "^2.2.12", + "@heroui/code": "^2.2.12", + "@heroui/date-input": "^2.3.15", + "@heroui/date-picker": "^2.3.16", + "@heroui/divider": "^2.2.11", + "@heroui/drawer": "^2.2.13", + "@heroui/dropdown": "^2.3.16", + "@heroui/form": "^2.1.15", + "@heroui/image": "^2.2.10", + "@heroui/input": "^2.4.16", + "@heroui/input-otp": "^2.1.15", + "@heroui/kbd": "^2.2.12", + "@heroui/link": "^2.2.13", + "@heroui/listbox": "^2.3.15", + "@heroui/menu": "^2.2.15", + "@heroui/modal": "^2.2.13", + "@heroui/navbar": "^2.2.14", + "@heroui/pagination": "^2.2.14", + "@heroui/popover": "^2.3.16", + "@heroui/progress": "^2.2.12", + "@heroui/radio": "^2.3.15", + "@heroui/ripple": "^2.2.12", + "@heroui/scroll-shadow": "^2.3.10", + "@heroui/select": "^2.4.16", + "@heroui/skeleton": "^2.2.10", + "@heroui/slider": "^2.4.13", + "@heroui/snippet": "^2.2.17", + "@heroui/spinner": "^2.2.13", + "@heroui/system": "^2.4.12", + "@heroui/table": "^2.2.15", + "@heroui/tabs": "^2.2.13", + "@heroui/theme": "^2.4.12", + "@heroui/tooltip": "^2.2.13", + "@heroui/user": "^2.2.12", + "@iconify-json/cib": "^1.2.2", + "@iconify-json/solar": "^1.2.2", + "@iconify/tailwind": "^1.2.0", + "@microsoft/clarity": "^1.0.0", + "@tanstack/react-query": "^5.69.0", + "@tanstack/react-query-devtools": "^5.69.0", + "@tanstack/react-router": "^1.114.22", + "@tanstack/react-router-devtools": "^1.114.22", + "@tanstack/react-router-with-query": "^1.114.25", + "@tanstack/react-start": "^1.114.22", + "@tanstack/zod-adapter": "^1.114.34", + "axios": "^1.8.3", + "framer-motion": "^11.18.2", + "react": "^19.0.0", + "react-cookie": "^8.0.1", + "react-dom": "^19.0.0", + "redaxios": "^0.5.1", + "sonner": "^2.0.1", + "tailwind-merge": "^2.6.0", + "vinxi": "0.5.3", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.5.4", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "tanstack-router-sitemap": "^1.0.12", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/conocenos.webp b/public/conocenos.webp new file mode 100644 index 0000000..799f908 Binary files /dev/null and b/public/conocenos.webp differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..1a17516 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/helvetiaLogo.svg b/public/helvetiaLogo.svg new file mode 100644 index 0000000..6654b87 --- /dev/null +++ b/public/helvetiaLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..a12ef50 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,9 @@ +{ + "name": "", + "short_name": "", + "icons": [ + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..0be9ce5 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1 @@ +https://victoriaseguros.com/seguros/decesosdaily1.0https://victoriaseguros.com/seguros/saluddaily1.0https://victoriaseguros.com/seguros/mascotasdaily1.0https://victoriaseguros.com/seguros/vehiculosdaily1.0https://victoriaseguros.com/seguros/vidadaily1.0https://victoriaseguros.com/seguros/hogardaily1.0https://victoriaseguros.com/formulariodaily1.0 \ No newline at end of file diff --git a/src/TextAnimations/BlurText/BlurText.tsx b/src/TextAnimations/BlurText/BlurText.tsx new file mode 100644 index 0000000..fc72f87 --- /dev/null +++ b/src/TextAnimations/BlurText/BlurText.tsx @@ -0,0 +1,129 @@ +/* + Installed from https://reactbits.dev/ts/tailwind/ +*/ + +import { useRef, useEffect, useState } from "react"; +import { useSprings, animated, SpringValue } from "@react-spring/web"; + +const AnimatedSpan = animated.span as React.FC< + React.HTMLAttributes +>; + +interface BlurTextProps { + text?: string; + delay?: number; + className?: string; + animateBy?: "words" | "letters"; + direction?: "top" | "bottom"; + threshold?: number; + rootMargin?: string; + animationFrom?: Record; + animationTo?: Record[]; + easing?: (t: number) => number | string; + onAnimationComplete?: () => void; +} + +const BlurText: React.FC = ({ + text = "", + delay = 200, + className = "", + animateBy = "words", + direction = "top", + threshold = 0.1, + rootMargin = "0px", + animationFrom, + animationTo, + easing = "easeOutCubic", + onAnimationComplete, +}) => { + const elements = animateBy === "words" ? text.split(" ") : text.split(""); + const [inView, setInView] = useState(false); + const ref = useRef(null); + const animatedCount = useRef(0); + + // Default animations based on direction + const defaultFrom: Record = + direction === "top" + ? { + filter: "blur(10px)", + opacity: 0, + transform: "translate3d(0,-50px,0)", + } + : { + filter: "blur(10px)", + opacity: 0, + transform: "translate3d(0,50px,0)", + }; + + const defaultTo: Record[] = [ + { + filter: "blur(5px)", + opacity: 0.5, + transform: + direction === "top" ? "translate3d(0,5px,0)" : "translate3d(0,-5px,0)", + }, + { filter: "blur(0px)", opacity: 1, transform: "translate3d(0,0,0)" }, + ]; + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setInView(true); + if (ref.current) { + observer.unobserve(ref.current); + } + } + }, + { threshold, rootMargin }, + ); + + if (ref.current) { + observer.observe(ref.current); + } + + return () => observer.disconnect(); + }, [threshold, rootMargin]); + + const springs = useSprings( + elements.length, + elements.map((_, i) => ({ + from: animationFrom || defaultFrom, + to: inView + ? async ( + next: (arg: Record>) => Promise, + ) => { + for (const step of animationTo || defaultTo) { + await next(step); + } + animatedCount.current += 1; + if ( + animatedCount.current === elements.length && + onAnimationComplete + ) { + onAnimationComplete(); + } + } + : animationFrom || defaultFrom, + delay: i * delay, + config: { easing: easing as any }, + })), + ); + + return ( +

+ {springs.map((props, index) => ( + + {elements[index] === " " ? "\u00A0" : elements[index]} + {animateBy === "words" && index < elements.length - 1 && "\u00A0"} + + ))} +

+ ); +}; + +export default BlurText; diff --git a/src/TextAnimations/CountUp/CountUp.tsx b/src/TextAnimations/CountUp/CountUp.tsx new file mode 100644 index 0000000..9f14c71 --- /dev/null +++ b/src/TextAnimations/CountUp/CountUp.tsx @@ -0,0 +1,116 @@ +/* + Installed from https://reactbits.dev/ts/tailwind/ +*/ + +import { useEffect, useRef } from "react"; +import { useInView, useMotionValue, useSpring } from "framer-motion"; + +interface CountUpProps { + to: number; + from?: number; + direction?: "up" | "down"; + delay?: number; + duration?: number; + className?: string; + startWhen?: boolean; + separator?: string; + onStart?: () => void; + onEnd?: () => void; +} + +export default function CountUp({ + to, + from = 0, + direction = "up", + delay = 0, + duration = 2, // Duration of the animation in seconds + className = "", + startWhen = true, + separator = "", + onStart, + onEnd, +}: CountUpProps) { + const ref = useRef(null); + const motionValue = useMotionValue(direction === "down" ? to : from); + + // Calculate damping and stiffness based on duration + const damping = 20 + 40 * (1 / duration); // Adjust this formula for finer control + const stiffness = 100 * (1 / duration); // Adjust this formula for finer control + + const springValue = useSpring(motionValue, { + damping, + stiffness, + }); + + const isInView = useInView(ref, { once: true, margin: "0px" }); + + // Set initial text content to the initial value based on direction + useEffect(() => { + if (ref.current) { + ref.current.textContent = String(direction === "down" ? to : from); + } + }, [from, to, direction]); + + // Start the animation when in view and startWhen is true + useEffect(() => { + if (isInView && startWhen) { + if (typeof onStart === "function") { + onStart(); + } + + const timeoutId = setTimeout(() => { + motionValue.set(direction === "down" ? from : to); + }, delay * 1000); + + const durationTimeoutId = setTimeout( + () => { + if (typeof onEnd === "function") { + onEnd(); + } + }, + delay * 1000 + duration * 1000, + ); + + return () => { + clearTimeout(timeoutId); + clearTimeout(durationTimeoutId); + }; + } + }, [ + isInView, + startWhen, + motionValue, + direction, + from, + to, + delay, + onStart, + onEnd, + duration, + ]); + + // Update text content with formatted number on spring value change + useEffect(() => { + const unsubscribe = springValue.on("change", (latest) => { + if (ref.current) { + const options = { + useGrouping: !!separator, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }; + + const formattedNumber = Intl.NumberFormat("en-US", options).format( + Number(latest.toFixed(0)), + ); + + ref.current.textContent = separator + ? formattedNumber.replace(/,/g, separator) + : formattedNumber; + } + }); + + return () => unsubscribe(); + }, [springValue, separator]); + + return ; +} diff --git a/src/TextAnimations/RotatingText/RotatingText.tsx b/src/TextAnimations/RotatingText/RotatingText.tsx new file mode 100644 index 0000000..a82af6e --- /dev/null +++ b/src/TextAnimations/RotatingText/RotatingText.tsx @@ -0,0 +1,276 @@ +/* + Installed from https://reactbits.dev/ts/tailwind/ +*/ + +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from "react"; +import { + motion, + AnimatePresence, + Transition, + type VariantLabels, + type Target, + type AnimationControls, + type TargetAndTransition, +} from "framer-motion"; + +function cn(...classes: (string | undefined | null | boolean)[]): string { + return classes.filter(Boolean).join(" "); +} + +export interface RotatingTextRef { + next: () => void; + previous: () => void; + jumpTo: (index: number) => void; + reset: () => void; +} + +export interface RotatingTextProps + extends Omit< + React.ComponentPropsWithoutRef, + "children" | "transition" | "initial" | "animate" | "exit" + > { + texts: string[]; + transition?: Transition; + initial?: boolean | Target | VariantLabels; + animate?: boolean | VariantLabels | AnimationControls | TargetAndTransition; + exit?: Target | VariantLabels; + animatePresenceMode?: "sync" | "wait"; + animatePresenceInitial?: boolean; + rotationInterval?: number; + staggerDuration?: number; + staggerFrom?: "first" | "last" | "center" | "random" | number; + loop?: boolean; + auto?: boolean; + splitBy?: string; + onNext?: (index: number) => void; + mainClassName?: string; + splitLevelClassName?: string; + elementLevelClassName?: string; +} + +const RotatingText = 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, + ...rest + }, + ref, + ) => { + const [currentTextIndex, setCurrentTextIndex] = useState(0); + + const splitIntoCharacters = (text: string): string[] => { + if (typeof Intl !== "undefined" && Intl.Segmenter) { + const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); + return Array.from( + segmenter.segment(text), + (segment) => segment.segment, + ); + } + return Array.from(text); + }; + + const elements = useMemo(() => { + const currentText: string = texts[currentTextIndex]; + if (splitBy === "characters") { + const words = currentText.split(" "); + return words.map((word, i) => ({ + characters: splitIntoCharacters(word), + needsSpace: i !== words.length - 1, + })); + } + if (splitBy === "words") { + return currentText.split(" ").map((word, i, arr) => ({ + characters: [word], + needsSpace: i !== arr.length - 1, + })); + } + if (splitBy === "lines") { + return currentText.split("\n").map((line, i, arr) => ({ + characters: [line], + needsSpace: i !== arr.length - 1, + })); + } + + return currentText.split(splitBy).map((part, i, arr) => ({ + characters: [part], + needsSpace: i !== arr.length - 1, + })); + }, [texts, currentTextIndex, splitBy]); + + const getStaggerDelay = useCallback( + (index: number, totalChars: number): 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 as number) - index) * staggerDuration; + }, + [staggerFrom, staggerDuration], + ); + + const handleIndexChange = useCallback( + (newIndex: number) => { + setCurrentTextIndex(newIndex); + if (onNext) 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]); + + 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]} + + + + + ); + }, +); + +RotatingText.displayName = "RotatingText"; +export default RotatingText; diff --git a/src/TextAnimations/TextCursor/TextCursor.tsx b/src/TextAnimations/TextCursor/TextCursor.tsx new file mode 100644 index 0000000..6850967 --- /dev/null +++ b/src/TextAnimations/TextCursor/TextCursor.tsx @@ -0,0 +1,174 @@ +/* + Installed from https://reactbits.dev/ts/tailwind/ +*/ + +import React, { useState, useEffect, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; + +interface TextCursorProps { + text: string; + delay?: number; + spacing?: number; + followMouseDirection?: boolean; + randomFloat?: boolean; + exitDuration?: number; + removalInterval?: number; + maxPoints?: number; +} + +interface TrailItem { + id: number; + x: number; + y: number; + angle: number; + randomX?: number; + randomY?: number; + randomRotate?: number; +} + +const TextCursor: React.FC = ({ + text = "⚛️", + delay = 0.01, + spacing = 100, + followMouseDirection = true, + randomFloat = true, + exitDuration = 0.5, + removalInterval = 30, + maxPoints = 5, +}) => { + const [trail, setTrail] = useState([]); + const containerRef = useRef(null); + const lastMoveTimeRef = useRef(Date.now()); + const idCounter = useRef(0); + + const handleMouseMove = (e: MouseEvent) => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + setTrail((prev) => { + let newTrail = [...prev]; + if (newTrail.length === 0) { + newTrail.push({ + id: idCounter.current++, + x: mouseX, + y: mouseY, + angle: 0, + ...(randomFloat && { + randomX: Math.random() * 10 - 5, + randomY: Math.random() * 10 - 5, + randomRotate: Math.random() * 10 - 5, + }), + }); + } else { + const last = newTrail[newTrail.length - 1]; + const dx = mouseX - last.x; + const dy = mouseY - last.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance >= spacing) { + let rawAngle = (Math.atan2(dy, dx) * 180) / Math.PI; + if (rawAngle > 90) rawAngle -= 180; + else if (rawAngle < -90) rawAngle += 180; + const computedAngle = followMouseDirection ? rawAngle : 0; + const steps = Math.floor(distance / spacing); + for (let i = 1; i <= steps; i++) { + const t = (spacing * i) / distance; + const newX = last.x + dx * t; + const newY = last.y + dy * t; + newTrail.push({ + id: idCounter.current++, + x: newX, + y: newY, + angle: computedAngle, + ...(randomFloat && { + randomX: Math.random() * 10 - 5, + randomY: Math.random() * 10 - 5, + randomRotate: Math.random() * 10 - 5, + }), + }); + } + } + } + if (newTrail.length > maxPoints) { + newTrail = newTrail.slice(newTrail.length - maxPoints); + } + return newTrail; + }); + lastMoveTimeRef.current = Date.now(); + }; + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + container.addEventListener("mousemove", handleMouseMove); + return () => container.removeEventListener("mousemove", handleMouseMove); + }, []); + + useEffect(() => { + const interval = setInterval(() => { + if (Date.now() - lastMoveTimeRef.current > 100) { + setTrail((prev) => (prev.length > 0 ? prev.slice(1) : prev)); + } + }, removalInterval); + return () => clearInterval(interval); + }, [removalInterval]); + + return ( +
+
+ + {trail.map((item) => ( + + {text} + + ))} + +
+
+ ); +}; + +export default TextCursor; diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..8b9fef1 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,6 @@ +import { + createStartAPIHandler, + defaultAPIFileRouteHandler, +} from '@tanstack/react-start/api' + +export default createStartAPIHandler(defaultAPIFileRouteHandler) diff --git a/src/client.tsx b/src/client.tsx new file mode 100644 index 0000000..1593d1b --- /dev/null +++ b/src/client.tsx @@ -0,0 +1,8 @@ +/// +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/react-start' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document, ) diff --git a/src/components/ButtonCall.tsx b/src/components/ButtonCall.tsx new file mode 100644 index 0000000..04d838e --- /dev/null +++ b/src/components/ButtonCall.tsx @@ -0,0 +1,50 @@ +import { Button } from "@heroui/button"; +import { getRouteApi, Link } from "@tanstack/react-router"; + +const routeApi = getRouteApi("/"); + +function ButtonCall({ + text = "Solicitar llamada", + secure, +}: { + text?: string; + secure: string; +}) { + const navigate = routeApi.useNavigate(); + const waUrl = `https://wa.me/${+34633620767}`; + return ( +
+ +
+ ); +} + +export default ButtonCall; diff --git a/src/components/Cookies.tsx b/src/components/Cookies.tsx new file mode 100644 index 0000000..98a6f72 --- /dev/null +++ b/src/components/Cookies.tsx @@ -0,0 +1,58 @@ +import { Button } from "@heroui/button"; +import { useEffect, useState } from "react"; +import { useCookies } from "react-cookie"; + +export default function CookieConsent() { + const [cookies, setCookie, removeCookie] = useCookies(["cookieConsent"]); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (!cookies.cookieConsent) { + setVisible(true); + } + }, [cookies]); + + const acceptCookies = () => { + setCookie("cookieConsent", "true", { + path: "/", + maxAge: 60 * 60 * 24 * 365, + }); + setVisible(false); + }; + + // const rejectCookies = () => { + // removeCookie("cookieConsent", { path: "/" }); + // setVisible(false); + // }; + + if (!visible) return null; + + return ( +
+

+ Utilizamos cookies para mejorar tu experiencia en el sitio. Al continuar + navegando, aceptas nuestra{" "} + + política de cookies + + / + + + política de privacidad + + . +

+
+
+ + {/* */} +
+
+
+ ); +} diff --git a/src/components/DefaultCatchBoundary.tsx b/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000..f750e7b --- /dev/null +++ b/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error('DefaultCatchBoundary Error:', error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..e483840 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,76 @@ +import { Chip } from "@heroui/chip"; +import { Image } from "@heroui/image"; +import { Link } from "@tanstack/react-router"; + +export const Footer = () => { + return ( + + ); +}; diff --git a/src/components/Kpi.tsx b/src/components/Kpi.tsx new file mode 100644 index 0000000..9c664bf --- /dev/null +++ b/src/components/Kpi.tsx @@ -0,0 +1,105 @@ +import { Card } from "@heroui/card"; +import { Chip } from "@heroui/chip"; +import { cn } from "@heroui/theme"; + +type TrendCardProps = { + title: string; + value: string; + change: string; + changeType: "positive" | "neutral" | "negative"; + trendType: "up" | "neutral" | "down"; + trendChipPosition?: "top" | "bottom"; + trendChipVariant?: "flat" | "light"; +}; + +const data: TrendCardProps[] = [ + { + title: "Hogar", + value: "+300", + change: "0.0%", + changeType: "neutral", + trendType: "neutral", + }, + { + title: "Decesos", + value: "+1500", + change: "1.0%", + changeType: "positive", + trendType: "up", + }, + { + title: "Vehículos", + value: "+200", + change: "1.0%", + changeType: "positive", + trendType: "up", + }, + { + title: "Mascotas", + value: "+100", + change: "1.0%", + changeType: "positive", + trendType: "up", + }, +]; + +const TrendCard = ({ + title, + value, + change, + changeType, + trendType, + trendChipPosition = "top", + trendChipVariant = "light", +}: TrendCardProps) => { + return ( + +
+
+
{title}
+
{value}
+
+ + ) : trendType === "neutral" ? ( + + ) : ( + + ) + } + variant={trendChipVariant} + > + {change} + +
+
+ ); +}; + +export default function Kpi() { + return ( +
+ {data.map((props, index) => ( + + ))} +
+ ); +} diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..3e7e664 --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,124 @@ +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + useDisclosure, +} from "@heroui/modal"; + +import { Form } from "@heroui/form"; +import { Input, Textarea } from "@heroui/input"; +import { Select, SelectItem } from "@heroui/select"; + +interface PropsForm { + // TODO: Replace zod infer + name: string; + time: string; + description: string; + email: string; +} + +import { Button } from "@heroui/button"; +import { toast } from "sonner"; +import { useMutation } from "@tanstack/react-query"; +import axios from "axios"; + +export default function ModalComponent() { + const { isOpen, onOpen, onOpenChange } = useDisclosure(); + + const { mutate } = useMutation({ + mutationKey: ["send-email"], + mutationFn: async (data: PropsForm) => { + const { name, time, description, email } = data; + await axios.post( + "https://sender-nr0t.onrender.com/sender", + { + to: "p3n4lv3r@gmail.com", + subject: "Correo decesos", + message: ` +

Nombre: ${name}

+

Horario: ${time}

+
+

Descripción: ${description}

+

Email: ${email}

+ `, + }, + { + headers: { + "Content-Type": "application/json", + "User-Agent": "insomnia/10.3.1", + }, + } + ); + }, + onSuccess: () => { + toast.success("Petición enviada", { id: "email-toast" }); + }, + onError: () => { + toast.error("Error al enviar petición", { id: "email-toast" }); + }, + }); + + const onSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + const name = formData.get("nombre") as string; + const time = formData.get("horario") as string; + const description = formData.get("descripcion") as string; + const email = formData.get("email") as string; + + toast.loading("Enviando", { id: "email-toast" }); + + mutate({ name, time, description, email }); + }; + + return ( + <> + {/* */} + + + + {() => ( + <> +
+ + Formulario contacto + + +
+ + +