From ae0be1c48e67463cc3dd26d6e1167d6491beb43f Mon Sep 17 00:00:00 2001 From: Feedback Studios Date: Tue, 16 Jun 2026 06:25:07 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20premium=20dark=20'Signal'=20homepage=20?= =?UTF-8?q?=E2=80=94=20EN=20revenue=20content,=20generated=20assets,=20SEO?= =?UTF-8?q?/AEO,=20animated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/CaseStudies.tsx | 67 ++ app/components/CountUp.tsx | 74 ++ app/components/Faq.tsx | 62 ++ app/components/Hero.tsx | 113 ++- app/components/Iridescence.tsx | 57 +- app/components/Marquee.tsx | 35 +- app/components/Metrics.tsx | 44 + app/components/Packages.tsx | 98 -- app/components/Process.tsx | 81 ++ app/components/Services.tsx | 37 + app/components/ServicesJourney.tsx | 163 ---- app/components/SiteHeader.tsx | 18 +- app/content.ts | 137 +++ app/globals.css | 1348 ++++++++++++++++----------- app/layout.tsx | 84 +- app/page.tsx | 225 +++-- content/home-content.md | 83 ++ package-lock.json | 466 ++++----- package.json | 6 +- public/assets/case-1.png | Bin 0 -> 1547737 bytes public/assets/case-2.png | Bin 0 -> 1307323 bytes public/assets/case-3.png | Bin 0 -> 1516513 bytes public/assets/hero-iridescent-1.png | Bin 0 -> 2120851 bytes 23 files changed, 1907 insertions(+), 1291 deletions(-) create mode 100644 app/components/CaseStudies.tsx create mode 100644 app/components/CountUp.tsx create mode 100644 app/components/Faq.tsx create mode 100644 app/components/Metrics.tsx delete mode 100644 app/components/Packages.tsx create mode 100644 app/components/Process.tsx create mode 100644 app/components/Services.tsx delete mode 100644 app/components/ServicesJourney.tsx create mode 100644 app/content.ts create mode 100644 content/home-content.md create mode 100644 public/assets/case-1.png create mode 100644 public/assets/case-2.png create mode 100644 public/assets/case-3.png create mode 100644 public/assets/hero-iridescent-1.png diff --git a/app/components/CaseStudies.tsx b/app/components/CaseStudies.tsx new file mode 100644 index 0000000..60efa8a --- /dev/null +++ b/app/components/CaseStudies.tsx @@ -0,0 +1,67 @@ +"use client"; + +/** + * CaseStudies — large film-cell cards. Each is a real
with the + * generated capsule visual, a problem -> result narrative, the method, and the + * headline metric. Alternating media side creates an editorial rhythm; refined + * scale-on-hover on the image. Images are explicit-sized + lazy-loaded. + */ + +import Reveal from "./Reveal"; +import { cases } from "../content"; + +export default function CaseStudies() { + return ( +
+
+ +

Case studies

+

Proof, not promises.

+

+ A few of the businesses we've grown. Same approach every time: + tie the work to the revenue, then prove it. +

+
+ +
+ {cases.map((c, i) => ( + +
+
+ {c.tag} + {/* eslint-disable-next-line @next/next/no-img-element */} + {c.alt} +
+
+

{c.problem}

+

+ {c.result} +

+

{c.how}

+

+ {c.metricNum} + {c.metricLabel} +

+
+
+
+ ))} +
+ +
+ + View all work → + +
+
+
+ ); +} diff --git a/app/components/CountUp.tsx b/app/components/CountUp.tsx new file mode 100644 index 0000000..52102fb --- /dev/null +++ b/app/components/CountUp.tsx @@ -0,0 +1,74 @@ +"use client"; + +/** + * CountUp — animates a number from 0 to its target when it scrolls into view. + * Uses GSAP + ScrollTrigger, formats with optional prefix/suffix/decimals and + * tabular figures. Under reduced-motion it renders the final value immediately. + * The full value is also written to the DOM on mount so it is correct even if + * JS/animation never runs (accessible + SSR-safe). + */ + +import { useEffect, useRef } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +gsap.registerPlugin(ScrollTrigger); + +type Props = { + value: number; + prefix?: string; + suffix?: string; + decimals?: number; + className?: string; +}; + +export default function CountUp({ + value, + prefix = "", + suffix = "", + decimals = 0, + className = "", +}: Props) { + const ref = useRef(null); + + const format = (n: number) => + `${prefix}${n.toLocaleString("en-US", { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + })}${suffix}`; + + useEffect(() => { + const el = ref.current; + if (!el) return; + + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + el.textContent = format(value); + return; + } + + const obj = { n: 0 }; + el.textContent = format(0); + + const ctx = gsap.context(() => { + gsap.to(obj, { + n: value, + duration: 2, + ease: "power2.out", + scrollTrigger: { trigger: el, start: "top 85%", once: true }, + onUpdate: () => { + el.textContent = format(obj.n); + }, + }); + }, el); + + return () => ctx.revert(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, prefix, suffix, decimals]); + + // Render full value at SSR/first paint; the effect resets to 0 then animates. + return ( + + {format(value)} + + ); +} diff --git a/app/components/Faq.tsx b/app/components/Faq.tsx new file mode 100644 index 0000000..1d76c76 --- /dev/null +++ b/app/components/Faq.tsx @@ -0,0 +1,62 @@ +"use client"; + +/** + * Faq — accessible accordion built on real + +
+
+

{f.a}

+
+
+ + ); + })} + + + + ); +} diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 5c68f13..79e4c49 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -1,11 +1,20 @@ "use client"; +/** + * Hero — type IS the hero. A display headline at 8-14vw reveals line by line + * (manual masked split via KineticText), the iridescent wave asset floats as a + * tasteful accent layer, and a trust line anchors the claim. Parallax on the + * accent + headline as the hero scrolls out. + */ + import { useEffect, useRef } from "react"; import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; import KineticText from "./KineticText"; -import PillMark from "./PillMark"; import Magnetic from "./Magnetic"; +gsap.registerPlugin(ScrollTrigger); + export default function Hero() { const root = useRef(null); @@ -17,35 +26,22 @@ export default function Hero() { const ctx = gsap.context(() => { const tl = gsap.timeline({ defaults: { ease: "power4.out" } }); tl.from(".hero__eyebrow", { y: 18, opacity: 0, duration: 0.9, delay: 0.15 }) + .from(".hero__accent", { opacity: 0, scale: 1.06, duration: 1.4 }, 0) .from(".hero__sub", { y: 22, opacity: 0, duration: 0.9 }, "-=0.2") - .from( - ".hero__actions > *", - { y: 20, opacity: 0, stagger: 0.1, duration: 0.7 }, - "-=0.5" - ) - .from(".hero__meta", { opacity: 0, duration: 0.8 }, "-=0.4"); + .from(".hero__actions > *", { y: 20, opacity: 0, stagger: 0.1, duration: 0.7 }, "-=0.5") + .from(".hero__trust", { opacity: 0, duration: 0.8 }, "-=0.4"); - // parallax on the floating mark as you scroll the hero out - gsap.to(".hero__mark", { - yPercent: -22, + // parallax: accent drifts up, headline lifts + fades as hero scrolls out + gsap.to(".hero__accent", { + yPercent: -18, ease: "none", - scrollTrigger: { - trigger: el, - start: "top top", - end: "bottom top", - scrub: true, - }, + scrollTrigger: { trigger: el, start: "top top", end: "bottom top", scrub: true }, }); gsap.to(".hero__display", { - yPercent: 12, - opacity: 0.25, + yPercent: 14, + opacity: 0.18, ease: "none", - scrollTrigger: { - trigger: el, - start: "top top", - end: "bottom top", - scrub: true, - }, + scrollTrigger: { trigger: el, start: "top top", end: "bottom top", scrub: true }, }); }, el); @@ -54,68 +50,63 @@ export default function Hero() { return (
+ {/* iridescent wave accent — decorative texture layer */} + {/* eslint-disable-next-line @next/next/no-img-element */} + +

- - - - herramientas. - + +

- -

- Estrategia humana sobre nuestra propia plataforma de IA. - Web, SEO, ads y contenido — más rápido, más medible y a mejor coste que - una agencia tradicional. + We're a results-driven digital marketing agency. We run paid, SEO, + and content programs built around your revenue targets, + then show you what they returned. Every month.

-
-
-
Entrega
-
en días, no semanas
-
-
-
Reporting
-
cada acción medida
-
-
-
Infraestructura
-
propia, no alquilada
-
-
+

+ $40M+ in client revenue generated +

- + scroll
); } diff --git a/app/components/Metrics.tsx b/app/components/Metrics.tsx new file mode 100644 index 0000000..9d5fd12 --- /dev/null +++ b/app/components/Metrics.tsx @@ -0,0 +1,44 @@ +"use client"; + +/** + * Metrics — four headline results with animated count-up on scroll-in. + * The ROAS metric is the single emerald-accented "signature" number. + */ + +import Reveal from "./Reveal"; +import CountUp from "./CountUp"; +import { metrics } from "../content"; + +export default function Metrics() { + return ( +
+
+ +

The numbers

+

Results we can show you.

+
+ +
+ + {metrics.map((m) => ( +
+

+ +

+

{m.label}

+
+ ))} +
+
+
+
+ ); +} diff --git a/app/components/Packages.tsx b/app/components/Packages.tsx deleted file mode 100644 index c61df2f..0000000 --- a/app/components/Packages.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -/** - * Packages — four tiers, each named for an outcome rather than a task list. - * "Motor" is the featured tier (lifted, gradient spine). Cards reveal on scroll - * and the featured one carries a continuously-shifting gradient border. - */ - -import Reveal from "./Reveal"; - -type Pkg = { - n: string; - who: string; - price: string; - f: string[]; - featured?: boolean; -}; - -const packages: Pkg[] = [ - { - n: "Sitio AI-native", - who: "Necesitas web, ya", - price: "Proyecto", - f: ["Web en código, editable", "SEO base + tracking", "Entregada en tiempo récord"], - }, - { - n: "Base", - who: "Estás arrancando", - price: "Mensual", - f: ["Fundamentos SEO", "Contenido inicial", "Analítica conectada"], - }, - { - n: "Motor", - who: "Quieres crecer en serio", - price: "Mensual", - f: ["Ads + SEO + contenido", "Dashboard de resultados", "Optimización mensual", "Plataforma de IA incluida"], - featured: true, - }, - { - n: "Partner", - who: "Quieres escalar", - price: "Retainer", - f: ["Full-stack de crecimiento", "Automatización a medida", "Prioridad y roadmap", "Acceso directo al equipo"], - }, -]; - -export default function Packages() { - return ( -
- -
- ); -} diff --git a/app/components/Process.tsx b/app/components/Process.tsx new file mode 100644 index 0000000..41d8c27 --- /dev/null +++ b/app/components/Process.tsx @@ -0,0 +1,81 @@ +"use client"; + +/** + * Process — "The Feedback Loop". The intro column pins (sticky) while the four + * steps scroll past; each step lights up as it reaches the viewport center, and + * a serif count in the sticky column advances with it. This is the + * scrollytelling beat. Degrades to a plain stacked list under reduced-motion + * (all steps shown active, no pin behaviour) and on narrow screens. + */ + +import { useEffect, useRef, useState } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { processSteps } from "../content"; + +gsap.registerPlugin(ScrollTrigger); + +export default function Process() { + const root = useRef(null); + const [active, setActive] = useState(0); + + useEffect(() => { + const el = root.current; + if (!el) return; + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + setActive(processSteps.length - 1); + return; + } + + const ctx = gsap.context(() => { + gsap.utils.toArray(".pstep").forEach((step, i) => { + ScrollTrigger.create({ + trigger: step, + start: "top 60%", + end: "bottom 60%", + onToggle: (self) => { + if (self.isActive) setActive(i); + }, + }); + }); + }, el); + + return () => ctx.revert(); + }, []); + + return ( +
+
+
+

How it works

+

+ The Feedback Loop. +

+ +
+ +
    + {processSteps.map((p, i) => ( +
  1. +
    + {p.n} +

    {p.name}

    +
    +

    {p.desc}

    +
  2. + ))} +
+
+
+ ); +} diff --git a/app/components/Services.tsx b/app/components/Services.tsx new file mode 100644 index 0000000..cbcc9dd --- /dev/null +++ b/app/components/Services.tsx @@ -0,0 +1,37 @@ +"use client"; + +/** + * Services — deliberately broken editorial grid. Six services on a 6-column + * grid; the first two cards span 3 (breaking the safe 2-col-per-card rhythm), + * the rest span 2. Serif index numerals, spectrum top-line on hover. + */ + +import Reveal from "./Reveal"; +import { services } from "../content"; + +export default function Services() { + return ( +
+
+ +

What we do

+

How we grow your business.

+
+ + + {services.map((s, i) => ( +
+ 0{i + 1} +

{s.name}

+

{s.desc}

+
+ ))} +
+
+
+ ); +} diff --git a/app/components/ServicesJourney.tsx b/app/components/ServicesJourney.tsx deleted file mode 100644 index 9a66e9b..0000000 --- a/app/components/ServicesJourney.tsx +++ /dev/null @@ -1,163 +0,0 @@ -"use client"; - -/** - * ServicesJourney — pinned horizontal-scroll track of services. - * - * On desktop the section pins and the panels scroll sideways as you scroll - * down (GSAP ScrollTrigger pin + scrub), with a progress bar. On mobile / when - * the pin would be awkward, and under reduced-motion, it degrades to a normal - * vertical stack (no pin, panels just stack and reveal). Each panel is a real - *
so the content is accessible regardless of layout. - */ - -import { useEffect, useRef } from "react"; -import { gsap } from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; - -gsap.registerPlugin(ScrollTrigger); - -const services = [ - { - n: "01", - t: "Web & Desarrollo", - d: "Sitios construidos en código, veloces y 100% editables. Sin plantillas, sin atajos: arquitectura que escala contigo.", - tags: ["Next.js", "Headless", "Core Web Vitals"], - }, - { - n: "02", - t: "SEO & GEO", - d: "Posicionamiento técnico y de contenido — para Google y para los motores de IA. Reporting real, no humo.", - tags: ["Técnico", "Contenido", "Answer Engines"], - }, - { - n: "03", - t: "Paid Ads", - d: "Google, Meta, TikTok y LinkedIn. Creatividad asistida por IA y pujas optimizadas hacia el resultado, no el clic.", - tags: ["Performance", "Creatividad IA", "Atribución"], - }, - { - n: "04", - t: "Contenido", - d: "Piezas pensadas para rankear y para convertir. Producción a escala sin perder la voz de tu marca.", - tags: ["Editorial", "Vídeo", "Social"], - }, - { - n: "05", - t: "Diseño & Marca", - d: "Identidad y dirección de arte con criterio. Sistemas de diseño que viven en producto, no en un PDF.", - tags: ["Identidad", "Design System", "Motion"], - }, - { - n: "06", - t: "Automatización", - d: "Flujos que devuelven horas: captación de leads, reporting automático y operativa conectada de punta a punta.", - tags: ["Leads", "Reporting", "Ops"], - }, -]; - -export default function ServicesJourney() { - const section = useRef(null); - const track = useRef(null); - const bar = useRef(null); - - useEffect(() => { - const sec = section.current; - const trk = track.current; - if (!sec || !trk) return; - - const mm = gsap.matchMedia(); - - // Desktop with motion allowed: pin + horizontal scroll. - mm.add( - "(min-width: 900px) and (prefers-reduced-motion: no-preference)", - () => { - const distance = () => trk.scrollWidth - window.innerWidth; - - const tween = gsap.to(trk, { - x: () => -distance(), - ease: "none", - scrollTrigger: { - trigger: sec, - start: "top top", - end: () => "+=" + distance(), - scrub: 0.6, - pin: true, - invalidateOnRefresh: true, - onUpdate: (self) => { - if (bar.current) { - bar.current.style.transform = `scaleX(${self.progress})`; - } - }, - }, - }); - - // Per-panel inner reveal as it slides into center. - gsap.utils.toArray(".sjourney__panel").forEach((panel) => { - gsap.from(panel.querySelectorAll(".sjourney__reveal"), { - y: 40, - opacity: 0, - duration: 0.6, - stagger: 0.06, - ease: "power3.out", - scrollTrigger: { - trigger: panel, - containerAnimation: tween, - start: "left 80%", - }, - }); - }); - - return () => { - if (bar.current) bar.current.style.transform = "scaleX(0)"; - }; - } - ); - - // Mobile / reduced-motion: simple stacked reveals. - mm.add("(max-width: 899px)", () => { - gsap.utils.toArray(".sjourney__panel").forEach((panel) => { - gsap.from(panel, { - y: 36, - opacity: 0, - duration: 0.7, - ease: "power3.out", - scrollTrigger: { trigger: panel, start: "top 88%" }, - }); - }); - }); - - return () => mm.revert(); - }, []); - - return ( -
-
-

02 — Qué hacemos

-

- Todo tu crecimiento, un mismo sistema. -

- -
- -
-
- {services.map((s) => ( -
- {s.n} -

{s.t}

-

{s.d}

-
    - {s.tags.map((tag) => ( -
  • {tag}
  • - ))} -
-
- ))} -
-
-
- ); -} diff --git a/app/components/SiteHeader.tsx b/app/components/SiteHeader.tsx index 28e86bc..67e9489 100644 --- a/app/components/SiteHeader.tsx +++ b/app/components/SiteHeader.tsx @@ -10,10 +10,10 @@ import { useEffect, useRef, useState } from "react"; import PillMark from "./PillMark"; const links = [ - { href: "#ventaja", label: "Ventaja" }, - { href: "#servicios", label: "Servicios" }, - { href: "#sectores", label: "Sectores" }, - { href: "#paquetes", label: "Paquetes" }, + { href: "#services", label: "Services" }, + { href: "#work", label: "Work" }, + { href: "#process", label: "About" }, + { href: "#faq", label: "FAQ" }, ]; export default function SiteHeader() { @@ -44,14 +44,14 @@ export default function SiteHeader() { className={`site-header${scrolled ? " is-scrolled" : ""}${open ? " is-open" : ""}`} >
- + feedbackstudios -