Add testimonials SVGs, BackToTop component, BlogSection, and scroll sticky hook; migrate animations and styles

This commit is contained in:
Cesar Mendivil 2025-11-04 15:58:55 -07:00
parent bb3081f843
commit a3a436e6d9
79 changed files with 843 additions and 87 deletions

View File

@ -0,0 +1,165 @@
# Migración de Estilos y Comportamientos de Techwind
## Archivos Creados
### 1. `src/styles/techwind-animations.css`
Archivo CSS global con animaciones y helpers migrados desde Techwind SCSS.
**Contenido incluido:**
- **Tipografía**: `::selection`, defaults de `<p>` y headings
- **Animaciones**:
- `mover`: Animación de flotación vertical (1.5s infinite alternate)
- `animate`: Círculos animados que suben y rotan (para efectos de fondo hero)
- `ppb_kenburns`: Efecto Ken Burns para imágenes (zoom + pan)
- `sk-bounce`: Animación de rebote para spinner/preloader
- `scroll`: Scroll infinito horizontal para sliders de logos
- **Utilidades**:
- `.scrollbar-hide`: Oculta scrollbars nativas
- `.carousel-smooth`: Scroll suave para carruseles
- `.background-effect .circles li`: Círculos animados de fondo
- `.image-wrap`: Wrapper para efecto Ken Burns
- `.spinner`: Spinner de carga con double-bounce
- **Navegación de testimonios**:
- `.tns-nav`: Estilos para dots de paginación
- `.slider .slide-track`: Track de slider infinito
**Uso:**
```tsx
// Ya importado globalmente en main.tsx
import './styles/techwind-animations.css'
```
### 2. `src/components/BackToTop.tsx`
Botón "Volver arriba" que aparece al hacer scroll > 500px.
**Características:**
- Aparece/desaparece automáticamente según scroll
- Scroll suave al hacer clic
- Diseño responsive con Tailwind
- Icono SVG de flecha arriba
**Uso:**
```tsx
import BackToTop from '../components/BackToTop'
function Layout() {
return (
<>
{/* Tu contenido */}
<BackToTop />
</>
)
}
```
### 3. `src/hooks/useScrollSticky.ts`
Hook para detectar scroll y hacer navbar sticky.
**Características:**
- Retorna `true` cuando scroll > 50px
- Event listener optimizado con `passive: true`
- Limpieza automática de listeners
**Uso:**
```tsx
import { useScrollSticky } from '../hooks/useScrollSticky'
function Navbar() {
const isSticky = useScrollSticky()
return (
<nav className={`${isSticky ? 'nav-sticky bg-white shadow' : 'bg-transparent'}`}>
{/* Contenido del navbar */}
</nav>
)
}
```
## Clases CSS Disponibles
### Animaciones
#### `.mover`
Flotación vertical suave (usar en iconos, ilustraciones):
```tsx
<div className="mover">
<img src="icon.svg" alt="Floating icon" />
</div>
```
#### `.background-effect`
Círculos animados de fondo (para hero sections):
```tsx
<section className="relative background-effect">
<ul className="circles">
{Array.from({ length: 10 }).map((_, i) => (
<li key={i}></li>
))}
</ul>
{/* Contenido hero */}
</section>
```
#### `.image-wrap`
Efecto Ken Burns en imágenes (zoom + pan lento):
```tsx
<div className="image-wrap overflow-hidden">
<img src="business-photo.jpg" alt="Business" />
</div>
```
### Utilidades
#### `.scrollbar-hide`
Oculta scrollbars sin afectar funcionalidad:
```tsx
<div className="overflow-x-auto scrollbar-hide">
{/* Contenido scrollable */}
</div>
```
#### `.carousel-smooth`
Scroll suave para carruseles (ya usado en TestimonialsSection):
```tsx
<div className="flex overflow-x-auto carousel-smooth scrollbar-hide">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
```
## Estilos SCSS Migrados (Referencia Original)
Los siguientes archivos de Techwind fueron analizados y migrados:
1. **`custom/_general.scss`**: Tipografía, selección
2. **`custom/_fonts.scss`**: Imports de Google Fonts (Nunito, etc.)
3. **`custom/pages/_helper.scss`**: Animaciones (mover, kenburns, preloader, cookies)
4. **`custom/pages/_hero.scss`**: Background effects, círculos animados
5. **`custom/plugins/_testi.scss`**: Navegación de testimonios, slider infinito
6. **`custom/plugins/_swiper-slider.scss`**: Swiper pagination
7. **`custom/structure/_topnav.scss`**: Navbar sticky, menú responsive
8. **`assets/js/app.js`**: Back to top, scroll sticky, dark mode toggle
## Funcionalidades NO Migradas (Manuales)
Las siguientes funcionalidades del `app.js` de Techwind requieren implementación manual según necesidad:
- **Toggle Menu**: Menú hamburguesa (implementar según diseño del navbar)
- **Active Menu**: Resaltado de item activo según URL (usar React Router `useLocation`)
- **Dark Mode Toggle**: Switch de modo oscuro (implementar con contexto/estado global)
- **RTL Mode**: Cambio LTR/RTL (agregar si se necesita soporte multiidioma RTL)
- **Contact Form**: Validación de formulario (implementar con React Hook Form o similar)
- **Preloader**: Pantalla de carga inicial (agregar si se desea)
## Próximos Pasos Recomendados
1. **Aplicar `.mover` a iconos/ilustraciones** en Features y Hero sections
2. **Agregar círculos animados** de fondo en StreamingHeroSection
3. **Implementar navbar sticky** con `useScrollSticky` en ModernSaasHeader
4. **Aplicar efecto Ken Burns** a imágenes hero si se usan
5. **Verificar paginación de testimonios** con estilos `.tns-nav` si se cambia diseño
## Notas
- Los errores de linter en `techwind-animations.css` sobre `@apply` son normales; Vite + Tailwind los procesan correctamente
- El archivo CSS se carga globalmente en `main.tsx`, no requiere importarlo en componentes
- Los componentes BackToTop y useScrollSticky son drop-in replacements listos para usar

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 200.4 66.5" style="enable-background:new 0 0 200.4 66.5;" xml:space="preserve">
<style type="text/css">
.amazon-0{fill:#8C98A4;}
</style>
<g>
<path id="arrow_1_" class="amazon-0" d="M120.9,47.6c-12.1,5.1-25.2,7.6-37.1,7.6c-17.7,0-34.8-4.8-48.7-12.9c-1.2-0.7-2.1,0.6-1.1,1.5
c12.8,11.5,29.8,18.5,48.7,18.5c13.4,0,29-4.3,39.8-12.2C124.2,48.8,122.6,46.8,120.9,47.6z"/>
<path id="arrow" class="amazon-0" d="M114.1,43.1c-0.9,0.7-0.7,1.6,0.3,1.5c3.4-0.4,11.2-1.3,12.6,0.5c1.4,1.8-1.5,9-2.8,12.3
c-0.4,1,0.5,1.4,1.3,0.7c5.8-4.8,7.3-15,6-16.4C130.3,40,120.2,38.8,114.1,43.1z"/>
<path id="z" class="amazon-0" d="M135.1,30.8c-3.3-1.9-7.2-2.4-10.8-2.3l9.8-14c0.9-1.2,1.4-2,1.4-2.6V8.3c0-0.7-0.5-1-1.1-1h-18.9
c-0.6,0-1,0.5-1,1v4.2l0,0c0,0.7,0.5,1,1.1,1h9.9l-11.4,16.2c-0.7,1-0.7,2.2-0.7,2.9v4.3c0,0.7,0.7,1.3,1.3,0.9
c6.4-3.4,14.1-3.1,19.9,0c0.7,0.4,1.4-0.4,1.4-0.9v-4.4C136,31.8,135.8,31.3,135.1,30.8z"/>
<path id="m" class="amazon-0" d="M40.1,38.8h5.8c0.7,0,1.1-0.5,1.1-1V22.2c0-3.4-0.2-8.1,4-8.1c4.1,0,3.5,4.8,3.5,8.1v15.6
c0,0.6,0.5,1,1,1h5.8c0.7,0,1.1-0.5,1.1-1V22.2c0-1.7-0.1-4.2,0.6-5.7c0.6-1.5,2-2.4,3.4-2.4c1.7,0,3,0.6,3.3,2.5
c0.3,1.2,0.2,4.3,0.2,5.5v15.6c0,0.6,0.5,1,1,1h5.8c0.7,0,1.1-0.5,1.1-1V19.1c0-3.2,0.4-6.8-1.5-9.2c-1.6-2.2-4.3-3.3-6.6-3.3
c-3.3,0-6.5,1.8-7.9,5.5c-1.6-3.7-3.8-5.5-7.4-5.5c-3.5,0-6.1,1.8-7.5,5.5h-0.1V8.3c0-0.6-0.5-0.9-1-1h-5.3c-0.7,0-1.1,0.5-1.1,1
v29.4C39.1,38.3,39.5,38.7,40.1,38.8z"/>
<path id="o" class="amazon-0" d="M151.7,6.7c-8.3,0-12.9,7.2-12.9,16.3s4.6,16.4,12.9,16.4c8,0,13.1-7.2,13.1-16.1
C164.9,14.1,160.3,6.7,151.7,6.7z M151.7,33.3c-4.5,0-4.5-7.7-4.5-11.3c0-3.6,0.3-9.3,4.5-9.3c1.9,0,3.1,0.8,3.7,2.9
c0.7,2.3,0.8,5.3,0.8,7.8C156.4,27.2,156.2,33.3,151.7,33.3z"/>
<path id="n" class="amazon-0" d="M184.5,6.7c-4,0-6.2,2-7.8,6h-0.1V8.2c-0.1-0.5-0.6-0.8-1-0.8h-5.3c-0.6,0-1,0.5-1.1,0.9v29.4
c0,0.6,0.5,1,1,1h5.7c0.7,0,1.1-0.5,1.1-1V21.9c0-2,0.1-3.8,0.9-5.6c0.7-1.4,2-2.3,3.3-2.3c4,0,3.6,4.7,3.6,7.9v16
c0.1,0.5,0.5,0.9,1,0.9h5.8c0.6,0,1-0.4,1.1-0.9V19.4c0-2.9,0-6.8-1.5-9.1C189.6,7.6,187.1,6.7,184.5,6.7z"/>
<path id="a_1_" class="amazon-0" d="M99.6,18.9c-3.3,0.4-7.6,0.7-10.8,2c-3.6,1.6-6.1,4.7-6.1,9.4c0,6,3.7,8.9,8.6,8.9
c4.1,0,6.3-0.9,9.5-4.2c1,1.5,1.4,2.2,3.3,3.8c0.5,0.2,0.9,0.2,1.4-0.1l0,0c1.1-1,3.3-2.8,4.4-3.8c0.5-0.4,0.4-1,0-1.5
c-1-1.5-2.1-2.6-2.1-5.3v-8.9c0-3.8,0.3-7.3-2.5-9.9c-2.1-2.1-5.9-2.9-8.7-2.9c-5.5,0-11.5,2-12.8,8.7c-0.1,0.7,0.4,1.1,0.8,1.2
L90,17c0.6,0,0.9-0.6,1-1c0.5-2.3,2.4-3.4,4.6-3.4c1.2,0,2.5,0.5,3.3,1.5c0.8,1.2,0.7,2.8,0.7,4.2V18.9L99.6,18.9z M98.4,30.8
c-0.9,1.6-2.3,2.6-4,2.6c-2.2,0-3.5-1.7-3.5-4.2c0-4.9,4.4-5.8,8.6-5.8v1.2C99.6,27,99.6,28.9,98.4,30.8z"/>
<path id="a" class="amazon-0" d="M34.6,33.4c-1-1.5-2.1-2.6-2.1-5.3v-8.9c0-3.8,0.3-7.3-2.5-9.9c-2.2-2.1-5.9-2.9-8.7-2.9
c-5.5,0-11.5,2-12.8,8.7c-0.1,0.7,0.4,1.1,0.8,1.2l5.6,0.6c0.6,0,0.9-0.6,1-1c0.5-2.3,2.4-3.4,4.7-3.4c1.2,0,2.5,0.5,3.3,1.5
c0.8,1.2,0.7,2.8,0.7,4.2v0.7c-3.4,0.4-7.7,0.7-10.9,2c-3.6,1.6-6.1,4.7-6.1,9.4c0,6,3.7,8.9,8.6,8.9c4.1,0,6.3-0.9,9.5-4.2
c1,1.5,1.4,2.2,3.3,3.8c0.5,0.2,0.9,0.2,1.4-0.1l0,0c1.1-1,3.3-2.8,4.4-3.8C35.1,34.5,35,33.9,34.6,33.4z M23.4,30.8
c-0.9,1.6-2.3,2.6-4,2.6c-2.2,0-3.4-1.7-3.4-4.2c0-4.9,4.4-5.8,8.6-5.8v1.2C24.4,27,24.5,28.9,23.4,30.8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="80" height="24">
<rect fill="#1877f2" width="24" height="24" rx="3"/>
<path d="M15 8h-2c-.6 0-1 .4-1 1v2H9v2h3v6h2v-6h2.3l.4-2H14V9c0-.2.2-1 1-1h0z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 200.4 66.5" style="enable-background:new 0 0 200.4 66.5;" xml:space="preserve">
<style type="text/css">
.google-0{fill:#8c98a4;}
</style>
<g>
<path id="g_1_" class="google-0" d="M51.5,24.4H29v6.6h16c-0.8,9.3-8.6,13.4-15.9,13.4c-9.4,0-17.6-7.4-17.6-17.7
c0-10.1,7.8-17.9,17.6-17.9c7.6,0,12,4.8,12,4.8l4.6-4.8c0,0-6-6.6-16.9-6.6C14.9,2.1,4.2,13.8,4.2,26.5c0,12.4,10.1,24.5,25,24.5
c13.1,0,22.7-8.9,22.7-22.2C51.9,26,51.5,24.4,51.5,24.4L51.5,24.4z"/>
<path id="o_2_" class="google-0" d="M69.8,19.5c-9.2,0-15.8,7.2-15.8,15.6c0,8.6,6.4,15.9,16,15.9c8.7,0,15.7-6.5,15.7-15.7
C85.6,24.9,77.4,19.5,69.8,19.5L69.8,19.5z M69.9,25.8c4.5,0,8.8,3.7,8.8,9.5c0,5.8-4.2,9.5-8.8,9.5c-5,0-8.9-4-8.9-9.6
C60.9,29.8,64.8,25.8,69.9,25.8L69.9,25.8z"/>
<path id="o_1_" class="google-0" d="M104.2,19.5c-9.2,0-15.8,7.2-15.8,15.6c0,8.6,6.4,15.9,16,15.9c8.7,0,15.7-6.5,15.7-15.7
C120,24.9,111.8,19.5,104.2,19.5L104.2,19.5z M104.3,25.8c4.5,0,8.8,3.7,8.8,9.5c0,5.8-4.2,9.5-8.8,9.5c-5,0-8.9-4-8.9-9.6
C95.3,29.8,99.2,25.8,104.3,25.8L104.3,25.8z"/>
<path id="g" class="google-0" d="M137.9,19.6c-8.5,0-15.1,7.4-15.1,15.7c0,9.4,7.7,15.8,14.9,15.8c4.5,0,6.8-1.7,8.7-3.8v3.1
c0,5.4-3.3,8.7-8.3,8.7c-4.8,0-7.2-3.6-8.1-5.6l-6.1,2.5c2.1,4.5,6.4,9.2,14.1,9.2c8.4,0,14.8-5.3,14.8-16.4V20.5h-6.6v2.7
C144.4,21,141.6,19.6,137.9,19.6L137.9,19.6z M138.5,25.8c4.1,0,8.4,3.6,8.4,9.6c0,6.2-4.2,9.5-8.5,9.5c-4.5,0-8.7-3.7-8.7-9.4
C129.8,29.3,134.1,25.8,138.5,25.8L138.5,25.8z"/>
<path id="e" class="google-0" d="M182.4,19.5c-8,0-14.7,6.3-14.7,15.7c0,9.9,7.5,15.8,15.4,15.8c6.6,0,10.8-3.7,13.2-6.9l-5.5-3.7
c-1.4,2.2-3.8,4.3-7.7,4.3c-4.4,0-6.4-2.4-7.7-4.8l21.1-8.8l-1.1-2.6C193.3,23.7,188.6,19.5,182.4,19.5L182.4,19.5z M182.6,25.6
c2.9,0,4.9,1.5,5.9,3.4l-14,5.9C173.7,30.3,178.1,25.6,182.6,25.6L182.6,25.6z"/>
<path id="l" class="google-0" d="M157.7,50.1h6.9V3.7h-6.9V50.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="80" height="24">
<rect fill="#0A66C2" width="24" height="24" rx="3"/>
<path d="M6.5 9H9v7H6.5zM7.8 7.5a1.2 1.2 0 11-.002-2.399A1.2 1.2 0 017.8 7.5zM10.5 9h2.2v1h.03c.3-.6 1.05-1.2 2.16-1.2 2.31 0 2.74 1.5 2.74 3.45V16h-2.5v-3.1c0-.74-.01-1.7-1.03-1.7-1.04 0-1.2.81-1.2 1.65V16H10.5V9z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="80" height="24">
<rect x="0" y="0" width="10" height="10" fill="#f14f19"/>
<rect x="12" y="0" width="10" height="10" fill="#00a4ef"/>
<rect x="0" y="12" width="10" height="10" fill="#7fba00"/>
<rect x="12" y="12" width="10" height="10" fill="#ffb900"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<rect width="120" height="120" rx="16" fill="#eef2ff"/>
<g transform="translate(20,20)">
<circle cx="40" cy="24" r="18" fill="#c7d2fe" />
<rect x="6" y="52" width="68" height="18" rx="6" fill="#a5b4fc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<rect width="120" height="120" rx="16" fill="#eefdf7"/>
<g transform="translate(20,20)">
<circle cx="40" cy="24" r="18" fill="#bbf7d0" />
<rect x="6" y="52" width="68" height="18" rx="6" fill="#86efac" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<rect width="120" height="120" rx="16" fill="#fff7ed"/>
<g transform="translate(20,20)">
<circle cx="40" cy="24" r="18" fill="#ffd8a8" />
<rect x="6" y="52" width="68" height="18" rx="6" fill="#ffb86b" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<rect width="120" height="120" rx="16" fill="#f0f9ff"/>
<g transform="translate(20,20)">
<circle cx="40" cy="24" r="18" fill="#bae6fd" />
<rect x="6" y="52" width="68" height="18" rx="6" fill="#7dd3fc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,51 @@
import React, { useState, useEffect } from 'react'
export default function BackToTop() {
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 500) {
setIsVisible(true)
} else {
setIsVisible(false)
}
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [])
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
})
}
if (!isVisible) return null
return (
<button
onClick={scrollToTop}
id="back-to-top"
className="fixed bottom-5 end-5 z-50 size-10 text-center bg-indigo-600 text-white rounded-full flex items-center justify-center shadow-md hover:bg-indigo-700 transition-all duration-300"
aria-label="Volver arriba"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="size-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
</svg>
</button>
)
}

View File

@ -0,0 +1,95 @@
import React from 'react'
import Reveal from './Reveal'
interface BlogPost {
id: number
title: string
excerpt: string
image: string
link: string
}
const blogPosts: BlogPost[] = [
{
id: 1,
title: 'Design your apps in your own way',
excerpt: 'The phrasal sequence of the is now so that many campaign and benefit',
image: '/images/blog/01.jpg',
link: '#'
},
{
id: 2,
title: 'How apps is changing the IT world',
excerpt: 'The phrasal sequence of the is now so that many campaign and benefit',
image: '/images/blog/02.jpg',
link: '#'
},
{
id: 3,
title: 'Smartest Applications for Business',
excerpt: 'The phrasal sequence of the is now so that many campaign and benefit',
image: '/images/blog/03.jpg',
link: '#'
}
]
export default function BlogSection() {
return (
<section className="relative md:py-24 py-16">
<div className="container relative">
<Reveal durationMs={500} distance={20} threshold={0.1}>
<div className="grid md:grid-cols-12 grid-cols-1 items-center">
<div className="md:col-span-6">
<h6 className="text-indigo-600 text-sm font-bold uppercase mb-2">Blogs</h6>
<h3 className="mb-4 md:text-3xl md:leading-normal text-2xl leading-normal font-semibold">
Reads Our Latest <br /> News & Blog
</h3>
</div>
<div className="md:col-span-6">
<p className="text-slate-400 max-w-xl">
Start working with Tailwind CSS that can provide everything you need to generate awareness, drive traffic, connect.
</p>
</div>
</div>
</Reveal>
<div className="grid grid-cols-1 lg:grid-cols-3 md:grid-cols-2 mt-8 gap-[30px]">
{blogPosts.map((post, index) => (
<Reveal key={post.id} durationMs={500} distance={20} threshold={0.1} delayMs={index * 200}>
<div className="blog relative rounded-md shadow dark:shadow-gray-800 overflow-hidden">
<img
src={post.image}
alt={post.title}
onError={(e: any) => {
e.currentTarget.src = '/images/blog/placeholder.jpg'
}}
className="w-full h-auto"
/>
<div className="content p-6">
<a
href={post.link}
className="title h5 text-lg font-medium hover:text-indigo-600 duration-500 ease-in-out"
>
{post.title}
</a>
<p className="text-slate-400 mt-3">{post.excerpt}</p>
<div className="mt-4">
<a
href={post.link}
className="relative inline-block tracking-wide align-middle text-base text-center border-none after:content-[''] after:absolute after:h-px after:w-0 hover:after:w-full after:end-0 hover:after:end-auto after:bottom-0 after:start-0 after:duration-500 font-normal hover:text-indigo-600 after:bg-indigo-600 duration-500 ease-in-out"
>
Read More <i data-feather="arrow-right" className="inline-block w-4 h-4"></i>
</a>
</div>
</div>
</div>
</Reveal>
))}
</div>
</div>
</section>
)
}

View File

@ -229,27 +229,27 @@ const ModernSaasFooter: React.FC = () => {
<ul className="list-none md:text-end text-center space-x-2 md:space-x-4">
<li className="inline">
<a href="#">
<img src="https://shreethemes.in/techwind/layouts/assets/images/payments/american-ex.png" className="max-h-6 inline" title="American Express" alt="American Express" />
<img src="/images/payments/american-ex.png" className="max-h-6 inline" title="American Express" alt="American Express" />
</a>
</li>
<li className="inline">
<a href="#">
<img src="https://shreethemes.in/techwind/layouts/assets/images/payments/discover.png" className="max-h-6 inline" title="Discover" alt="Discover" />
<img src="/images/payments/discover.png" className="max-h-6 inline" title="Discover" alt="Discover" />
</a>
</li>
<li className="inline">
<a href="#">
<img src="https://shreethemes.in/techwind/layouts/assets/images/payments/master-card.png" className="max-h-6 inline" title="Master Card" alt="Master Card" />
<img src="/images/payments/master-card.png" className="max-h-6 inline" title="Master Card" alt="Master Card" />
</a>
</li>
<li className="inline">
<a href="#">
<img src="https://shreethemes.in/techwind/layouts/assets/images/payments/paypal.png" className="max-h-6 inline" title="Paypal" alt="Paypal" />
<img src="/images/payments/paypal.png" className="max-h-6 inline" title="Paypal" alt="Paypal" />
</a>
</li>
<li className="inline">
<a href="#">
<img src="https://shreethemes.in/techwind/layouts/assets/images/payments/visa.png" className="max-h-6 inline" title="Visa" alt="Visa" />
<img src="/images/payments/visa.png" className="max-h-6 inline" title="Visa" alt="Visa" />
</a>
</li>
</ul>

View File

@ -4,9 +4,10 @@
*/
import React, { useState, useEffect } from 'react'
import { useScrollSticky } from '../hooks/useScrollSticky'
const ModernSaasHeader: React.FC = () => {
const [isScrolled, setIsScrolled] = useState(false)
const isScrolled = useScrollSticky()
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const [activeDropdown, setActiveDropdown] = useState<string | null>(null)
const [isDarkMode, setIsDarkMode] = useState(false)
@ -24,12 +25,7 @@ const ModernSaasHeader: React.FC = () => {
document.documentElement.classList.remove('dark')
}
const handleScroll = () => {
setIsScrolled(window.scrollY > 50)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
// Removed scroll listener - now handled by useScrollSticky hook
}, [])
const toggleTheme = () => {

View File

@ -16,7 +16,7 @@ const testimonials: Testimonial[] = [
name: 'María García',
role: 'CEO',
company: 'TechStartup',
image: '/images/testimonials/user1.jpg',
image: '/images/testimonials/user1.svg',
quote: 'AvanzaCast transformó completamente la forma en que hacemos webinars. La calidad es excepcional y es súper fácil de usar.',
rating: 5
},
@ -25,7 +25,7 @@ const testimonials: Testimonial[] = [
name: 'Carlos Rodríguez',
role: 'Content Creator',
company: 'YouTube',
image: '/images/testimonials/user2.jpg',
image: '/images/testimonials/user2.svg',
quote: 'Llevo más de 2 años usando AvanzaCast para mis streams. No cambiaría a otra plataforma por nada del mundo.',
rating: 5
},
@ -34,7 +34,7 @@ const testimonials: Testimonial[] = [
name: 'Ana Martínez',
role: 'Marketing Director',
company: 'GlobalCorp',
image: '/images/testimonials/user3.jpg',
image: '/images/testimonials/user3.svg',
quote: 'La capacidad de transmitir simultáneamente a múltiples plataformas nos ha ayudado a triplicar nuestro alcance.',
rating: 5
},
@ -43,7 +43,7 @@ const testimonials: Testimonial[] = [
name: 'Juan Pérez',
role: 'Podcaster',
company: 'El Podcast Diario',
image: '/images/testimonials/user4.jpg',
image: '/images/testimonials/user4.svg',
quote: 'La calidad de audio es impresionante. Mis invitados siempre comentan lo profesional que se ve todo.',
rating: 5
}

View File

@ -1,56 +1,83 @@
'use client';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import feather from 'feather-icons';
import Reveal from './Reveal'
const features = [
{
id: 1,
icon: '📡',
icon: 'monitor',
title: 'Multistreaming Avanzado',
description: 'Transmite simultáneamente a YouTube, Twitch, Facebook, LinkedIn y más de 15 plataformas con un solo click.',
description: 'Transmite simultáneamente a YouTube, Twitch, Facebook, LinkedIn y más plataformas.',
benefits: ['Sin límites de plataformas', 'Configuración automática', 'Calidad HD/4K'],
},
{
id: 2,
icon: '☁️',
title: 'Grabación en la Nube',
description: 'Todas tus transmisiones se guardan automáticamente en la nube con almacenamiento ilimitado.',
benefits: ['Almacenamiento ilimitado', 'Respaldo automático', 'Descarga instantánea'],
icon: 'heart',
title: 'Estudio Virtual Profesional',
description: 'Estudio completo en la nube con escenas, overlays y transiciones profesionales.',
benefits: ['Escenas personalizadas', 'Transiciones suaves', 'Sin marca de agua'],
},
{
id: 3,
icon: '👥',
title: 'Invitados Remotos',
description: 'Invita hasta 10 participantes simultáneos con video HD y audio profesional de baja latencia.',
benefits: ['Hasta 10 invitados', 'Baja latencia', 'Sin instalación'],
icon: 'eye',
title: 'Chat Unificado en Vivo',
description: 'Gestiona todos los chats de tus plataformas en un solo lugar con moderación.',
benefits: ['Chat consolidado', 'Moderación IA', 'Respuestas rápidas'],
},
{
id: 4,
icon: '🎨',
title: 'Branding Personalizado',
description: 'Crea overlays, logos, banners y escenas personalizadas con nuestro editor de arrastrar y soltar.',
benefits: ['Editor visual', 'Plantillas profesionales', 'Sin marca de agua'],
icon: 'layout',
title: 'Invitados Remotos',
description: 'Invita hasta 10 participantes simultáneos con audio y video de alta calidad.',
benefits: ['Hasta 10 invitados', 'Baja latencia', 'Sin instalación'],
},
{
id: 5,
icon: '💬',
title: 'Chat Unificado',
description: 'Gestiona todos los chats de tus plataformas en un solo lugar con moderación automática.',
benefits: ['Chat unificado', 'Moderación IA', 'Respuestas rápidas'],
icon: 'feather',
title: 'Grabación Automática',
description: 'Todas tus transmisiones se graban automáticamente en la nube con calidad HD.',
benefits: ['Respaldo automático', 'Descarga ilimitada', 'Edición posterior'],
},
{
id: 6,
icon: '📊',
icon: 'code',
title: 'Branding Personalizado',
description: 'Diseña tu propio branding con logos, overlays y gráficos personalizados.',
benefits: ['Editor drag & drop', 'Plantillas pro', 'Logo permanente'],
},
{
id: 7,
icon: 'user-check',
title: 'Analytics en Tiempo Real',
description: 'Monitorea espectadores, engagement, comentarios y estadísticas detalladas durante el stream.',
benefits: ['Métricas en vivo', 'Reportes detallados', 'Exportación de datos'],
description: 'Monitorea audiencia, engagement y métricas detalladas durante tu stream.',
benefits: ['Métricas en vivo', 'Reportes detallados', 'Exportación datos'],
},
{
id: 8,
icon: 'globe',
title: 'Compatible con RTMP',
description: 'Transmite desde cualquier software o hardware compatible con RTMP/RTMPS.',
benefits: ['OBS Studio', 'vMix', 'Hardware encoders'],
},
{
id: 9,
icon: 'settings',
title: 'Scheduling Avanzado',
description: 'Programa tus transmisiones y automatiza anuncios en todas tus plataformas.',
benefits: ['Calendario integrado', 'Auto-notificaciones', 'Multi-plataforma'],
},
];
export default function StreamingFeatures() {
const [activeFeature, setActiveFeature] = useState(1);
useEffect(() => {
// Inicializar iconos de Feather
feather.replace();
}, []);
return (
<section className="py-20 bg-white dark:bg-slate-900 transition-colors duration-500">
<div className="container mx-auto px-4">
@ -69,7 +96,7 @@ export default function StreamingFeatures() {
<Reveal durationMs={600} distance={16} threshold={0.08} delayMs={index * 80}>
<div className="flex duration-500 hover:scale-105 shadow dark:shadow-gray-800 hover:shadow-md dark:hover:shadow-gray-700 ease-in-out items-center p-3 rounded-md bg-white dark:bg-slate-900 transition-all">
<div className="flex items-center justify-center h-[45px] min-w-[45px] -rotate-45 bg-gradient-to-r from-transparent to-indigo-600/10 text-indigo-600 dark:text-indigo-400 text-center rounded-full me-3 transition-colors duration-500">
<span className="text-2xl rotate-45">{feature.icon}</span>
<i data-feather={feature.icon} className="size-5 rotate-45"></i>
</div>
<div className="flex-1">
<h4 className="mb-0 text-lg font-medium text-slate-900 dark:text-white transition-colors duration-500">

View File

@ -13,7 +13,14 @@ export default function StreamingHeroSection() {
};
return (
<section className="relative before:content-[''] before:absolute xl:before:-top-[30%] lg:before:-top-[50%] sm:before:-top-[80%] before:-top-[90%] before:-start-80 before:end-0 before:w-[140rem] before:h-[65rem] before:-rotate-12 before:bg-indigo-600/5 dark:before:bg-indigo-600/10 before:z-0 items-center overflow-hidden pt-32 md:pt-44 pb-16">
<section className="relative before:content-[''] before:absolute xl:before:-top-[30%] lg:before:-top-[50%] sm:before:-top-[80%] before:-top-[90%] before:-start-80 before:end-0 before:w-[140rem] before:h-[65rem] before:-rotate-12 before:bg-indigo-600/5 dark:before:bg-indigo-600/10 before:z-0 items-center overflow-hidden pt-32 md:pt-44 pb-16 background-effect">
{/* Animated background circles */}
<ul className="circles">
{Array.from({ length: 10 }).map((_, i) => (
<li key={i}></li>
))}
</ul>
<div className="container mx-auto px-4 relative z-10">
<div className="grid grid-cols-1 text-center">
{/* Content */}
@ -58,7 +65,7 @@ export default function StreamingHeroSection() {
{/* Video Preview */}
<div className="aspect-video bg-gradient-to-br from-slate-900 to-slate-800 dark:from-slate-800 dark:to-slate-900 relative transition-colors duration-500">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<div className="text-center mover">
<div className="w-24 h-24 mx-auto mb-4 bg-gradient-to-br from-indigo-600 to-indigo-700 dark:from-indigo-500 dark:to-indigo-600 rounded-full flex items-center justify-center">
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h6a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zM14.553 7.106A1 1 0 0014 8v4a1 1 0 00.553.894l2 1A1 1 0 0018 13V7a1 1 0 00-1.447-.894l-2 1z" />

View File

@ -1,14 +1,12 @@
import React, { useEffect, useRef, useState } from 'react'
import Reveal from './Reveal'
interface Testimonial { text: string; author: string }
interface Testimonial { id: number; text: string; name: string; role?: string; company?: string; image?: string; rating?: number }
const testimonials: Testimonial[] = [
{ text: 'Esta probablemente sea la plataforma de transmisión más fácil de usar que conozco...', author: 'Bomeca Trotter' },
{ text: 'Uso AvanzaCast desde hace mucho tiempo y sigo eligiéndolo...', author: 'Krissy Buck' },
{ text: 'Hace dos años que uso este sistema y me encanta!', author: 'Joy Ann Lajeret' },
{ text: 'La integración con múltiples plataformas es perfecta...', author: 'Carlos Mendoza' },
{ text: 'Como creadora de contenido, necesitaba una herramienta confiable...', author: 'María González' }
{ id: 1, name: 'Calvin Carlo', role: 'Manager', company: '', image: '/images/clients/01.jpg', rating: 5, text: 'It seems that only fragments of the original text remain in the Lorem Ipsum texts used today.' },
{ id: 2, name: 'Christa Smith', role: 'Manager', company: '', image: '/images/clients/02.jpg', rating: 5, text: "The most well-known dummy text is the 'Lorem Ipsum', which is said to have originated in the 16th century." },
{ id: 3, name: 'Jemina C LOne', role: 'Manager', company: '', image: '/images/clients/03.jpg', rating: 5, text: 'One disadvantage of Lorum Ipsum is that in Latin certain letters appear more frequently than others.' },
]
export default function TestimonialsSection() {
@ -16,16 +14,68 @@ export default function TestimonialsSection() {
const [isAutoPlay, setIsAutoPlay] = useState(true)
const multiplier = 12
const duplicatedTestimonials = Array.from({ length: multiplier }, () => testimonials).flat()
const [activeIndex, setActiveIndex] = useState(0)
const itemWidthRef = useRef<number>(400)
const scrollLeft = () => { if (scrollRef.current) scrollRef.current.scrollBy({ left: -400, behavior: 'smooth' }) }
const scrollRight = () => { if (scrollRef.current) scrollRef.current.scrollBy({ left: 400, behavior: 'smooth' }) }
const scrollLeft = () => { if (scrollRef.current) scrollRef.current.scrollBy({ left: -itemWidthRef.current, behavior: 'smooth' }) }
const scrollRight = () => { if (scrollRef.current) scrollRef.current.scrollBy({ left: itemWidthRef.current, behavior: 'smooth' }) }
// Scroll to a canonical item index (0..testimonials.length-1) within the middle set
const goToIndex = (itemIndex: number) => {
if (!scrollRef.current) return
const container = scrollRef.current
const itemW = itemWidthRef.current || 400
const singleSetWidth = testimonials.length * itemW
const middleStart = singleSetWidth * Math.floor(multiplier / 2)
const target = middleStart + itemIndex * itemW
container.scrollTo({ left: target, behavior: 'smooth' })
}
useEffect(() => {
if (scrollRef.current) {
const singleSetWidth = testimonials.length * 400
// measure item width from DOM (first testimonial-item)
const container = scrollRef.current
const firstItem = container.querySelector('.testimonial-item') as HTMLElement | null
const ww = firstItem ? firstItem.clientWidth : 400
itemWidthRef.current = ww
const singleSetWidth = testimonials.length * ww
const middleStart = singleSetWidth * Math.floor(multiplier / 2)
scrollRef.current.scrollLeft = middleStart
// Calculate visible items based on viewport
const containerWidth = container.clientWidth
let visibleItems = 3 // desktop default
if (containerWidth < 768) visibleItems = 1 // mobile
else if (containerWidth < 1024) visibleItems = 2 // tablet
// Center the carousel by adding padding to show exact number of items
const totalVisibleWidth = ww * visibleItems
const sidePadding = Math.max(0, (containerWidth - totalVisibleWidth) / 2)
container.style.paddingLeft = `${sidePadding}px`
container.style.paddingRight = `${sidePadding}px`
container.scrollLeft = middleStart
}
// re-measure on resize
const onResize = () => {
if (!scrollRef.current) return
const container = scrollRef.current
const firstItem = container.querySelector('.testimonial-item') as HTMLElement | null
if (!firstItem) return
const ww = firstItem.clientWidth
itemWidthRef.current = ww
const containerWidth = container.clientWidth
let visibleItems = 3
if (containerWidth < 768) visibleItems = 1
else if (containerWidth < 1024) visibleItems = 2
const totalVisibleWidth = ww * visibleItems
const sidePadding = Math.max(0, (containerWidth - totalVisibleWidth) / 2)
container.style.paddingLeft = `${sidePadding}px`
container.style.paddingRight = `${sidePadding}px`
}
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
useEffect(() => {
@ -39,13 +89,18 @@ export default function TestimonialsSection() {
const container = scrollRef.current
const handleScroll = () => {
if (!container) return
const singleSetWidth = testimonials.length * 400
const itemW = itemWidthRef.current || 400
const singleSetWidth = testimonials.length * itemW
const totalWidth = singleSetWidth * multiplier
const middleStart = singleSetWidth * Math.floor(multiplier / 2)
const tolerance = 100
const tolerance = Math.max(50, itemW / 4)
requestAnimationFrame(() => {
if (container.scrollLeft <= tolerance) container.scrollLeft = middleStart
else if (container.scrollLeft >= totalWidth - container.clientWidth - tolerance) container.scrollLeft = middleStart
// compute visible index relative to middle set
const relative = Math.round((container.scrollLeft - middleStart) / itemW)
const idx = ((relative % testimonials.length) + testimonials.length) % testimonials.length
setActiveIndex(idx)
})
}
@ -57,32 +112,60 @@ export default function TestimonialsSection() {
}, [])
return (
<section className="bg-white py-20">
<section className="bg-gradient-to-b from-blue-50 to-white py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl lg:text-5xl font-black text-gray-900 mb-16">
<h3 className="mb-4 md:text-3xl text-2xl md:leading-normal leading-normal font-semibold">
Ya se crearon más de 60 millones de transmisiones y grabaciones en AvanzaCast
</h2>
</h3>
<p className="text-slate-400 max-w-xl mx-auto mb-10">Start working with Tailwind CSS that can provide everything you need to generate awareness, drive traffic, connect.</p>
<div className="relative w-full">
<button onClick={scrollLeft} className="absolute left-4 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full bg-white shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 cursor-pointer transition-all">
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7"/></svg>
</button>
{/* Navigation arrows at extremes */}
{/* Arrows removed — navigation via pagination dots only */}
<button onClick={scrollRight} className="absolute right-4 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full bg-white shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 cursor-pointer transition-all">
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7"/></svg>
</button>
<div ref={scrollRef} className="flex space-x-8 overflow-x-auto scrollbar-hide carousel-smooth pb-4 px-16" onMouseEnter={() => setIsAutoPlay(false)} onMouseLeave={() => setIsAutoPlay(true)}>
{duplicatedTestimonials.map((testimonial, index) => {
<div ref={scrollRef} className="flex overflow-x-auto scrollbar-hide carousel-smooth pb-8" onMouseEnter={() => setIsAutoPlay(false)} onMouseLeave={() => setIsAutoPlay(true)}>
{duplicatedTestimonials.map((testimonial, index) => {
const setNumber = Math.floor(index / testimonials.length)
const itemIndex = index % testimonials.length
const base = testimonials[itemIndex]
return (
<div key={`testimonial-set-${setNumber}-item-${itemIndex}-${testimonial.author}`} className="flex-shrink-0 w-80">
<div key={`testimonial-set-${setNumber}-item-${itemIndex}-${base.id}`} className="testimonial-item flex-shrink-0 w-full md:w-1/2 lg:w-1/3 px-4">
<Reveal durationMs={500} distance={12} threshold={0.05}>
<div className="bg-gray-50 p-8 rounded-2xl h-full">
<p className="text-gray-700 italic mb-6 leading-relaxed text-sm">&ldquo;{testimonial.text}&rdquo;</p>
<p className="font-semibold text-gray-900">{testimonial.author}</p>
</div>
<div>
<div className="relative bg-white p-8 pb-6 rounded-lg shadow-sm transition-shadow border border-gray-100 flex flex-col">
<div className="absolute -top-1 left-6 opacity-20 z-0 pointer-events-none">
<svg width="60" height="60" viewBox="0 0 100 100" fill="currentColor" className="text-indigo-600">
<path d="M20,45 Q20,20 35,15 Q30,25 30,35 Q30,45 40,45 L40,60 Q20,60 20,45 Z"/>
<path d="M55,45 Q55,20 70,15 Q65,25 65,35 Q65,45 75,45 L75,60 Q55,60 55,45 Z"/>
</svg>
</div>
<p className="text-slate-400 mb-3 leading-relaxed text-base relative z-10 flex-grow">&ldquo;{base.text}&rdquo;</p>
<div className="flex items-center justify-center mt-1 mb-2">
{Array.from({ length: base.rating || 5 }).map((_, i) => (
<svg key={i} className="w-4 h-4 mx-0.5" viewBox="0 0 24 24" fill="#F59E0B" stroke="#F59E0B"><path d="M12 .587l3.668 7.431L24 9.75l-6 5.848L19.335 24 12 19.898 4.665 24 6 15.598 0 9.75l8.332-1.732z"/></svg>
))}
</div>
</div>
<div className="mt-3 text-center relative">
<div className="absolute -top-10 left-1/2 -translate-x-1/2 w-20 h-20 sm:w-22 sm:h-22 md:w-24 md:h-24 z-30">
<img src={base.image} alt={base.name} onError={(e:any)=>{ e.currentTarget.src = '/images/testimonials/user1.svg' }} className="mx-auto rounded-full w-20 h-20 sm:w-22 sm:h-22 md:w-24 md:h-24 object-cover shadow-2xl ring-4 ring-white" />
</div>
<div className="pt-12 md:pt-14">
<p className="mt-2 font-semibold text-gray-900">{base.name}</p>
<p className="text-slate-400 text-sm">{base.role}</p>
</div>
</div>
{/* small pointer triangle */}
<div className="flex justify-center mt-2 z-10">
<svg width="28" height="14" viewBox="0 0 28 14" fill="none" xmlns="http://www.w3.org/2000/svg" className="-mt-4">
<path d="M0 0 L14 14 L28 0 Z" fill="#ffffff" />
</svg>
</div>
</div>
</Reveal>
</div>
)
@ -90,20 +173,18 @@ export default function TestimonialsSection() {
</div>
</div>
<div className="flex justify-center space-x-4 mt-8">
<button onClick={() => setIsAutoPlay(!isAutoPlay)} className="text-sm text-gray-500 hover:text-gray-700 flex items-center space-x-1 transition-colors">
{isAutoPlay ? (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 9v6m4-6v6"/></svg>
<span>Pausar auto-scroll</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M15 14h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span>Reanudar auto-scroll</span>
</>
)}
</button>
<div className="flex flex-col items-center mt-8">
<div className="flex items-center gap-3">
{testimonials.map((t, idx) => (
<button
key={t.id}
onClick={() => goToIndex(idx)}
aria-label={`Ir al testimonio ${idx + 1}`}
aria-current={activeIndex === idx}
className={`transition-all rounded-full ${activeIndex === idx ? 'bg-indigo-600 w-8 h-3' : 'bg-gray-300 w-3 h-3'} hover:scale-110`}
/>
))}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,23 @@
import { useEffect, useState } from 'react'
/**
* Hook para navbar sticky - migrado de Techwind
* Agrega clase 'nav-sticky' al navbar cuando el scroll > 50px
*/
export function useScrollSticky() {
const [isSticky, setIsSticky] = useState(false)
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY || document.documentElement.scrollTop
setIsSticky(scrollTop >= 50)
}
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll() // Check initial state
return () => window.removeEventListener('scroll', handleScroll)
}, [])
return isSticky
}

View File

@ -2,6 +2,7 @@ import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './index.css'
import './styles/techwind-animations.css'
createRoot(document.getElementById('root')!).render(
<React.StrictMode>

View File

@ -12,8 +12,9 @@ import StreamingFeatures from '../components/StreamingFeatures'
import PlatformLogos from '../components/PlatformLogos'
import TestimonialsSection from '../components/TestimonialsSection'
import PricingSection from '../components/PricingSection'
import BlogSection from '../components/BlogSection'
import ModernSaasFooter from '../components/ModernSaasFooter'
import ScrollToTop from '../components/ScrollToTop'
import BackToTop from '../components/BackToTop'
const NextreamLanding: React.FC = () => {
useEffect(() => {
@ -41,11 +42,14 @@ const NextreamLanding: React.FC = () => {
{/* Pricing Section */}
<PricingSection />
{/* Blog Section */}
<BlogSection />
{/* Modern SaaS Footer */}
<ModernSaasFooter />
{/* Scroll to Top Button */}
<ScrollToTop />
{/* Back to Top Button - Migrated from Techwind */}
<BackToTop />
</div>
)
}

View File

@ -0,0 +1,199 @@
/* Techwind Migrated Styles - Animations & Helpers */
/* Selection */
::selection {
@apply bg-indigo-600/90 text-white;
}
/* Typography defaults */
p {
@apply leading-relaxed;
}
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
@apply leading-normal;
}
/* Mover animation */
.mover {
animation: mover 1.5s infinite alternate;
}
@keyframes mover {
0% {
transform: translateY(0);
}
100% {
transform: translateY(10px);
}
}
/* Smooth scrollbar hide */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Carousel smooth scroll */
.carousel-smooth {
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
/* Background animated circles (hero effects) */
@keyframes animate {
0% {
transform: translateY(0) rotate(0deg);
opacity: 1;
border-radius: 10px;
}
100% {
transform: translateY(-1000px) rotate(720deg);
opacity: 0;
}
}
.background-effect .circles li {
@apply absolute block -bottom-[150px] bg-indigo-600/30;
animation: animate 25s linear infinite;
}
.background-effect .circles li:nth-child(1) {
@apply size-12 start-1/4;
animation-delay: 0s;
}
.background-effect .circles li:nth-child(2) {
@apply size-12 start-[10%];
animation-delay: 2s;
animation-duration: 12s;
}
.background-effect .circles li:nth-child(3) {
@apply size-12 start-[70%];
animation-delay: 4s;
}
.background-effect .circles li:nth-child(4) {
@apply size-12 start-[40%];
animation-delay: 0s;
animation-duration: 18s;
}
.background-effect .circles li:nth-child(5) {
@apply size-12 start-[65%];
animation-delay: 0s;
}
.background-effect .circles li:nth-child(6) {
@apply size-12 start-3/4;
animation-delay: 3s;
}
.background-effect .circles li:nth-child(7) {
@apply size-12 start-[35%];
animation-delay: 7s;
}
.background-effect .circles li:nth-child(8) {
@apply size-12 start-1/2;
animation-delay: 15s;
animation-duration: 45s;
}
.background-effect .circles li:nth-child(9) {
@apply size-12 start-[20%];
animation-delay: 2s;
animation-duration: 35s;
}
.background-effect .circles li:nth-child(10) {
@apply size-12 start-[85%];
animation-delay: 0s;
animation-duration: 11s;
}
/* Kenburn effect for images */
.image-wrap {
animation: 100s ppb_kenburns linear infinite alternate;
}
@keyframes ppb_kenburns {
0% {
transform: scale(1.3) translate(-10%, 10%);
}
25% {
transform: scale(1) translate(0, 0);
}
50% {
transform: scale(1.3) translate(10%, 10%);
}
75% {
transform: scale(1) translate(0, 0);
}
100% {
transform: scale(1.3) translate(-10%, 10%);
}
}
/* Preloader spinner */
@keyframes sk-bounce {
0%, 100% {
transform: scale(0.0);
}
50% {
transform: scale(1.0);
}
}
.spinner .double-bounce1,
.spinner .double-bounce2 {
@apply w-full h-full rounded-full bg-indigo-600/60 absolute top-0 start-0;
animation: sk-bounce 2.0s infinite ease-in-out;
}
.spinner .double-bounce2 {
animation-delay: -1.0s;
}
/* Dark mode toggle switch */
.label .ball {
transition: transform 0.2s linear;
@apply translate-x-0;
}
.checkbox:checked + .label .ball {
@apply translate-x-6;
}
/* Testimonial navigation dots (pagination) */
.tns-nav {
@apply text-center mt-3;
}
.tns-nav button {
@apply rounded-[3px] bg-indigo-600/30 duration-500 border-0 m-1 p-[5px];
}
.tns-nav button.tns-nav-active {
@apply bg-indigo-600 rotate-[45deg];
}
/* Smooth infinite scroll slider */
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-360px * 6));
}
}
.slider .slide-track {
animation: scroll 120s linear infinite;
width: calc(360px * 20);
}