Add testimonials SVGs, BackToTop component, BlogSection, and scroll sticky hook; migrate animations and styles
165
packages/landing-page/TECHWIND_MIGRATION.md
Normal 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
|
||||
BIN
packages/landing-page/public/images/blog/01.jpg
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
packages/landing-page/public/images/blog/02.jpg
Normal file
|
After Width: | Height: | Size: 423 KiB |
BIN
packages/landing-page/public/images/blog/03.jpg
Normal file
|
After Width: | Height: | Size: 398 KiB |
BIN
packages/landing-page/public/images/blog/04.jpg
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
packages/landing-page/public/images/blog/05.jpg
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
packages/landing-page/public/images/blog/06.jpg
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
packages/landing-page/public/images/blog/07.jpg
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
packages/landing-page/public/images/blog/08.jpg
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
packages/landing-page/public/images/blog/09.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
packages/landing-page/public/images/blog/10.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
packages/landing-page/public/images/blog/11.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/landing-page/public/images/blog/12.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
packages/landing-page/public/images/blog/13.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
packages/landing-page/public/images/blog/14.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
packages/landing-page/public/images/blog/bg.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
packages/landing-page/public/images/blog/bg1.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
packages/landing-page/public/images/blog/slide02.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
packages/landing-page/public/images/clients/01.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/landing-page/public/images/clients/02.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
packages/landing-page/public/images/clients/03.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
packages/landing-page/public/images/clients/04.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/landing-page/public/images/clients/05.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/landing-page/public/images/clients/06.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/landing-page/public/images/clients/07.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/landing-page/public/images/clients/08.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/landing-page/public/images/clients/10.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
packages/landing-page/public/images/clients/11.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
packages/landing-page/public/images/clients/12.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
packages/landing-page/public/images/clients/13.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
packages/landing-page/public/images/clients/14.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
packages/landing-page/public/images/clients/9.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
38
packages/landing-page/public/images/clients/amazon.svg
Normal 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 |
BIN
packages/landing-page/public/images/clients/android.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
packages/landing-page/public/images/clients/circle-logo.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
4
packages/landing-page/public/images/clients/facebook.svg
Normal 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 |
BIN
packages/landing-page/public/images/clients/google-logo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
27
packages/landing-page/public/images/clients/google.svg
Normal 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 |
BIN
packages/landing-page/public/images/clients/lenovo-logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
packages/landing-page/public/images/clients/linkedin.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
4
packages/landing-page/public/images/clients/linkedin.svg
Normal 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 |
@ -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 |
BIN
packages/landing-page/public/images/clients/shree-logo.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
packages/landing-page/public/images/clients/skype.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
packages/landing-page/public/images/clients/snapchat.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
packages/landing-page/public/images/clients/spotify.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
packages/landing-page/public/images/clients/telegram.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
packages/landing-page/public/images/clients/whatsapp.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
packages/landing-page/public/images/payments/american-ex.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
packages/landing-page/public/images/payments/discover.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
packages/landing-page/public/images/payments/fund.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
packages/landing-page/public/images/payments/master-card.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
packages/landing-page/public/images/payments/paypal.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
packages/landing-page/public/images/payments/visa.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
packages/landing-page/public/images/testimonials/01.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/landing-page/public/images/testimonials/02.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
packages/landing-page/public/images/testimonials/03.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
packages/landing-page/public/images/testimonials/04.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/landing-page/public/images/testimonials/05.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/landing-page/public/images/testimonials/06.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/landing-page/public/images/testimonials/07.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/landing-page/public/images/testimonials/08.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
51
packages/landing-page/src/components/BackToTop.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
95
packages/landing-page/src/components/BlogSection.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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">“{testimonial.text}”</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">“{base.text}”</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>
|
||||
|
||||
23
packages/landing-page/src/hooks/useScrollSticky.ts
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
199
packages/landing-page/src/styles/techwind-animations.css
Normal 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);
|
||||
}
|
||||