first commit

This commit is contained in:
2026-02-11 16:31:54 +00:00
parent b5ca8ed23f
commit a1f63588a0
40 changed files with 3238 additions and 82 deletions

122
src/app/LeadCaptureForm.tsx Normal file
View File

@@ -0,0 +1,122 @@
"use client";
import React, { useState } from "react";
import { track } from "@/lib/analytics";
export default function LeadCaptureForm() {
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [nome, setNome] = useState("");
const [email, setEmail] = useState("");
const [perfil, setPerfil] = useState("");
const [objetivo, setObjetivo] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("loading");
track("submit_lead_capture", { perfil });
try {
const res = await fetch("/api/waiting-list", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nome, email, perfil, objetivo }),
});
if (!res.ok) throw new Error("Erro ao submeter.");
setStatus("success");
setNome("");
setEmail("");
setPerfil("");
setObjetivo("");
} catch {
setStatus("error");
}
};
if (status === "success") {
return (
<div role="alert" className="rounded-xl border border-accent/30 bg-accent/5 p-6 text-center">
<p className="text-lg font-semibold text-accent">Inscrição confirmada!</p>
<p className="mt-2 text-sm text-muted-foreground">
Entraremos em contacto em breve. Obrigado pelo interesse.
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4 text-left">
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label htmlFor="lc-nome" className="block text-sm font-medium mb-1.5">
Nome *
</label>
<input
id="lc-nome"
type="text"
required
value={nome}
onChange={(e) => setNome(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition"
/>
</div>
<div>
<label htmlFor="lc-email" className="block text-sm font-medium mb-1.5">
Email *
</label>
<input
id="lc-email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition"
/>
</div>
</div>
<div>
<label htmlFor="lc-perfil" className="block text-sm font-medium mb-1.5">
Perfil *
</label>
<select
id="lc-perfil"
required
value={perfil}
onChange={(e) => setPerfil(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition"
>
<option value="">Selecionar...</option>
<option value="atleta">Atleta</option>
<option value="treinador">Treinador</option>
<option value="clube">Clube</option>
</select>
</div>
<div>
<label htmlFor="lc-objetivo" className="block text-sm font-medium mb-1.5">
Objetivo (opcional)
</label>
<input
id="lc-objetivo"
type="text"
value={objetivo}
onChange={(e) => setObjetivo(e.target.value)}
placeholder="Ex: Melhorar pacing na maratona"
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition"
/>
</div>
{status === "error" && (
<div role="alert" className="text-sm text-red-500 bg-red-500/5 rounded-lg p-3">
Ocorreu um erro. Tenta novamente.
</div>
)}
<button
type="submit"
disabled={status === "loading"}
className="w-full rounded-xl bg-accent text-white font-semibold py-3 px-6 hover:bg-accent-dark shadow-lg shadow-accent/20 transition-all disabled:opacity-50 cursor-pointer"
>
{status === "loading" ? "A enviar..." : "Entrar na lista de espera"}
</button>
</form>
);
}

View File

@@ -0,0 +1,41 @@
import { NextResponse } from "next/server";
import { addDemoRequest } from "@/lib/submissions";
export async function POST(request: Request) {
try {
const body = await request.json();
const { nome, email, papel, organizacao, numAtletas, mensagem } = body;
if (!nome || !email || !papel) {
return NextResponse.json(
{ error: "Nome, email e papel são obrigatórios." },
{ status: 400 }
);
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: "Email inválido." },
{ status: 400 }
);
}
addDemoRequest({
nome,
email,
papel,
organizacao: organizacao ?? "",
numAtletas: numAtletas ?? "",
mensagem: mensagem ?? "",
createdAt: new Date().toISOString(),
});
return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ error: "Erro ao processar o pedido." },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,40 @@
import { NextResponse } from "next/server";
import { addWaitingListEntry } from "@/lib/submissions";
export async function POST(request: Request) {
try {
const body = await request.json();
const { nome, email, perfil, objetivo, distancia } = body;
if (!nome || !email || !perfil) {
return NextResponse.json(
{ error: "Nome, email e perfil são obrigatórios." },
{ status: 400 }
);
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: "Email inválido." },
{ status: 400 }
);
}
addWaitingListEntry({
nome,
email,
perfil,
distancia: distancia ?? "",
objetivo: objetivo ?? "",
createdAt: new Date().toISOString(),
});
return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ error: "Erro ao processar o pedido." },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,18 @@
"use client";
import { useEffect } from "react";
import { track } from "@/lib/analytics";
export default function BlogPostTracker({
slug,
title,
}: {
slug: string;
title: string;
}) {
useEffect(() => {
track("blog_post_open", { slug, title });
}, [slug, title]);
return null;
}

View File

@@ -0,0 +1,149 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import CTASection from "@/components/CTASection";
import { getPostBySlug, getAllPosts } from "@/lib/blog-data";
import BlogPostTracker from "./BlogPostTracker";
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
return getAllPosts().map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return { title: "Artigo não encontrado" };
return {
title: post.title,
description: post.description,
};
}
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) notFound();
// Simple markdown-like rendering for the body
const renderBody = (body: string) => {
return body.split("\n").map((line, i) => {
if (line.startsWith("## ")) {
return (
<h2 key={i} className="text-2xl font-bold mt-10 mb-4">
{line.replace("## ", "")}
</h2>
);
}
if (line.startsWith("### ")) {
return (
<h3 key={i} className="text-xl font-bold mt-8 mb-3">
{line.replace("### ", "")}
</h3>
);
}
if (line.startsWith("> ")) {
return (
<blockquote
key={i}
className="border-l-4 border-accent pl-4 py-2 my-6 text-muted-foreground italic"
>
{line.replace("> ", "")}
</blockquote>
);
}
if (line.startsWith("- **")) {
const match = line.match(/- \*\*(.+?)\*\*\s*[—–-]\s*(.+)/);
if (match) {
return (
<li key={i} className="flex items-start gap-2 ml-4 mb-2">
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-accent shrink-0" />
<span>
<strong>{match[1]}</strong> {match[2]}
</span>
</li>
);
}
}
if (line.startsWith("- ")) {
return (
<li key={i} className="flex items-start gap-2 ml-4 mb-2">
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-accent shrink-0" />
<span>{line.replace("- ", "")}</span>
</li>
);
}
if (line.startsWith("**Exemplo")) {
return (
<p key={i} className="font-bold mt-6 mb-2">{line.replace(/\*\*/g, "")}</p>
);
}
if (line.trim() === "") {
return <div key={i} className="h-2" />;
}
return (
<p key={i} className="text-muted-foreground leading-relaxed mb-2">
{line}
</p>
);
});
};
return (
<>
<BlogPostTracker slug={post.slug} title={post.title} />
<PageHero title={post.title}>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<time dateTime={post.date}>
{new Date(post.date).toLocaleDateString("pt-PT", {
day: "numeric",
month: "long",
year: "numeric",
})}
</time>
<span>·</span>
<span>{post.readingTime} de leitura</span>
</div>
<div className="flex flex-wrap gap-2 mt-3">
{post.tags.map((tag) => (
<span
key={tag}
className="text-xs font-medium px-2.5 py-1 rounded-full bg-accent/10 text-accent"
>
{tag}
</span>
))}
</div>
</PageHero>
<Section>
<article className="max-w-3xl mx-auto">
{renderBody(post.body)}
</article>
<div className="max-w-3xl mx-auto mt-16 pt-8 border-t border-border">
<Link
href="/blog"
className="text-sm text-accent hover:text-accent-dark font-medium flex items-center gap-2"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Voltar ao blog
</Link>
</div>
</Section>
<CTASection
title="Dados no campo de visão"
description="Vê as tuas métricas sem desviar os olhos do treino."
/>
</>
);
}

79
src/app/blog/page.tsx Normal file
View File

@@ -0,0 +1,79 @@
import type { Metadata } from "next";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import { getAllPosts } from "@/lib/blog-data";
export const metadata: Metadata = {
title: "Blog",
description:
"Artigos sobre performance, treino e biomecânica para corredores e treinadores.",
};
function BlogPostCard({
slug,
title,
description,
date,
tags,
readingTime,
}: {
slug: string;
title: string;
description: string;
date: string;
tags: string[];
readingTime: string;
}) {
return (
<article className="group rounded-2xl border border-border bg-card p-6 md:p-8 transition-all duration-200 hover:shadow-lg hover:border-accent/30 hover:-translate-y-1">
<div className="flex flex-wrap gap-2 mb-4">
{tags.map((tag) => (
<span
key={tag}
className="text-xs font-medium px-2.5 py-1 rounded-full bg-accent/10 text-accent"
>
{tag}
</span>
))}
</div>
<h2 className="text-xl font-bold mb-2 group-hover:text-accent transition-colors">
<Link href={`/blog/${slug}`}>{title}</Link>
</h2>
<p className="text-muted-foreground text-sm leading-relaxed mb-4">
{description}
</p>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<time dateTime={date}>
{new Date(date).toLocaleDateString("pt-PT", {
day: "numeric",
month: "long",
year: "numeric",
})}
</time>
<span>{readingTime} de leitura</span>
</div>
</article>
);
}
export default function BlogPage() {
const posts = getAllPosts();
return (
<>
<PageHero
title="Blog"
description="Artigos sobre performance, treino e biomecânica. Conhecimento para correr melhor."
/>
<Section>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<BlogPostCard key={post.slug} {...post} />
))}
</div>
</Section>
</>
);
}

View File

@@ -0,0 +1,123 @@
"use client";
import Tabs from "@/components/Tabs";
import Card from "@/components/Card";
import HUDPreviewSVG from "@/components/HUDPreviewSVG";
const casosDeUso = [
{
id: "pista",
label: "Pista",
content: (
<div className="space-y-8">
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 className="text-2xl font-bold mb-4">Treino de pista com precisão absoluta</h3>
<p className="text-muted-foreground leading-relaxed mb-4">
A pista é o laboratório do corredor. Distâncias exatas, superfície uniforme e controlo total. O HUD acrescenta uma camada de informação que elimina a necessidade de relógio ou cronómetro manual.
</p>
<ul className="space-y-2">
{["Splits por volta automáticos", "Contagem regressiva de intervalos", "Ritmo atual vs. ritmo-alvo", "Recuperação cronometrada"].map((item, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="h-1.5 w-1.5 rounded-full bg-accent" />
{item}
</li>
))}
</ul>
</div>
<HUDPreviewSVG variant="interval" />
</div>
<div className="grid sm:grid-cols-3 gap-4">
{[
{ title: "Intervalos curtos", desc: "200m400m com recuperação programada." },
{ title: "Intervalos longos", desc: "800m2000m para VO2máx e limiar." },
{ title: "Tempo runs", desc: "Ritmo sustentado com controlo visual." },
].map((item, i) => (
<Card key={i}>
<h4 className="font-bold mb-1">{item.title}</h4>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</Card>
))}
</div>
</div>
),
},
{
id: "estrada",
label: "Estrada",
content: (
<div className="space-y-8">
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 className="text-2xl font-bold mb-4">Provas e longões com estratégia</h3>
<p className="text-muted-foreground leading-relaxed mb-4">
Na estrada, cada segundo conta. O HUD mantém o teu plano de splits sempre visível, permitindo ajustes instantâneos sem perder o ritmo.
</p>
<ul className="space-y-2">
{["Plano de splits por km visível", "Delta acumulado face ao objetivo", "Alertas de zona cardíaca", "Estimativa de tempo final"].map((item, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="h-1.5 w-1.5 rounded-full bg-accent" />
{item}
</li>
))}
</ul>
</div>
<HUDPreviewSVG variant="pace" />
</div>
<div className="grid sm:grid-cols-3 gap-4">
{[
{ title: "5K10K", desc: "Ritmo agressivo com gestão de esforço." },
{ title: "Meia-maratona", desc: "Even ou negative splits com controlo total." },
{ title: "Maratona", desc: "Gestão energética e pacing conservador nos primeiros km." },
].map((item, i) => (
<Card key={i}>
<h4 className="font-bold mb-1">{item.title}</h4>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</Card>
))}
</div>
</div>
),
},
{
id: "trail",
label: "Trail",
content: (
<div className="space-y-8">
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 className="text-2xl font-bold mb-4">Trail com gestão de esforço inteligente</h3>
<p className="text-muted-foreground leading-relaxed mb-4">
No trail, o ritmo é irrelevante em subidas íngremes. A potência torna-se a métrica dominante. O HUD adapta-se ao contexto e mostra o que realmente importa.
</p>
<ul className="space-y-2">
{["Potência como métrica principal em subida", "Desnível acumulado em tempo real", "Alertas de esforço excessivo", "Gestão energética para ultras"].map((item, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="h-1.5 w-1.5 rounded-full bg-accent" />
{item}
</li>
))}
</ul>
</div>
<HUDPreviewSVG variant="power" />
</div>
<div className="grid sm:grid-cols-3 gap-4">
{[
{ title: "Trail curto", desc: "Até 25 km. Gestão de subidas com watts." },
{ title: "Trail longo", desc: "2580 km. Pacing por esforço, não por ritmo." },
{ title: "Ultra", desc: "80+ km. Monitorização de fadiga a longo prazo." },
].map((item, i) => (
<Card key={i}>
<h4 className="font-bold mb-1">{item.title}</h4>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</Card>
))}
</div>
</div>
),
},
];
export default function CasosDeUsoTabs() {
return <Tabs tabs={casosDeUso} />;
}

View File

@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import CTASection from "@/components/CTASection";
import Button from "@/components/Button";
import CasosDeUsoTabs from "./CasosDeUsoTabs";
export const metadata: Metadata = {
title: "Casos de Uso",
description:
"De intervalos na pista a ultras de montanha — o HUD adapta-se ao teu contexto de corrida.",
};
export default function CasosDeUsoPage() {
return (
<>
<PageHero
title="Casos de uso"
description="De intervalos na pista a ultras de montanha — o HUD adapta-se ao teu contexto."
>
<Button href="/contactos" trackEvent="casos_pedir_demo">
Pedir demo
</Button>
</PageHero>
<Section>
<CasosDeUsoTabs />
</Section>
<CTASection />
</>
);
}

View File

@@ -0,0 +1,102 @@
"use client";
import React, { useState } from "react";
import { track } from "@/lib/analytics";
export default function DemoRequestForm() {
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [nome, setNome] = useState("");
const [email, setEmail] = useState("");
const [organizacao, setOrganizacao] = useState("");
const [papel, setPapel] = useState("");
const [numAtletas, setNumAtletas] = useState("");
const [mensagem, setMensagem] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("loading");
track("submit_demo_request", { papel });
try {
const res = await fetch("/api/demo-request", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nome, email, organizacao, papel, numAtletas, mensagem }),
});
if (!res.ok) throw new Error("Erro ao submeter.");
setStatus("success");
} catch {
setStatus("error");
}
};
if (status === "success") {
return (
<div role="alert" className="rounded-xl border border-accent/30 bg-accent/5 p-8 text-center">
<p className="text-xl font-semibold text-accent mb-2">Pedido enviado!</p>
<p className="text-muted-foreground">
Entraremos em contacto em breve para agendar a demonstração.
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-5">
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label htmlFor="demo-nome" className="block text-sm font-medium mb-1.5">Nome *</label>
<input id="demo-nome" type="text" required value={nome} onChange={(e) => setNome(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
<div>
<label htmlFor="demo-email" className="block text-sm font-medium mb-1.5">Email *</label>
<input id="demo-email" type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label htmlFor="demo-org" className="block text-sm font-medium mb-1.5">Organização / Clube (opcional)</label>
<input id="demo-org" type="text" value={organizacao} onChange={(e) => setOrganizacao(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
<div>
<label htmlFor="demo-papel" className="block text-sm font-medium mb-1.5">Papel *</label>
<select id="demo-papel" required value={papel} onChange={(e) => setPapel(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition">
<option value="">Selecionar...</option>
<option value="treinador">Treinador</option>
<option value="diretor_tecnico">Diretor técnico</option>
<option value="gestor_clube">Gestor de clube</option>
<option value="atleta">Atleta</option>
<option value="outro">Outro</option>
</select>
</div>
</div>
<div>
<label htmlFor="demo-atletas" className="block text-sm font-medium mb-1.5"> de atletas (opcional)</label>
<input id="demo-atletas" type="text" value={numAtletas} onChange={(e) => setNumAtletas(e.target.value)}
placeholder="Ex: 15-20"
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
<div>
<label htmlFor="demo-msg" className="block text-sm font-medium mb-1.5">Mensagem (opcional)</label>
<textarea id="demo-msg" rows={4} value={mensagem} onChange={(e) => setMensagem(e.target.value)}
placeholder="Conte-nos sobre o seu contexto e objectivos..."
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition resize-none" />
</div>
{status === "error" && (
<div role="alert" className="text-sm text-red-500 bg-red-500/5 rounded-lg p-3">
Ocorreu um erro. Tenta novamente.
</div>
)}
<button type="submit" disabled={status === "loading"}
className="w-full rounded-xl bg-accent text-white font-semibold py-3 px-6 hover:bg-accent-dark shadow-lg shadow-accent/20 transition-all disabled:opacity-50 cursor-pointer">
{status === "loading" ? "A enviar..." : "Pedir demonstração"}
</button>
</form>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
import React, { useState } from "react";
import { track } from "@/lib/analytics";
export default function WaitingListForm() {
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [nome, setNome] = useState("");
const [email, setEmail] = useState("");
const [perfil, setPerfil] = useState("");
const [distancia, setDistancia] = useState("");
const [objetivo, setObjetivo] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("loading");
track("submit_waiting_list", { perfil });
try {
const res = await fetch("/api/waiting-list", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nome, email, perfil, distancia, objetivo }),
});
if (!res.ok) throw new Error("Erro ao submeter.");
setStatus("success");
} catch {
setStatus("error");
}
};
if (status === "success") {
return (
<div role="alert" className="rounded-xl border border-accent/30 bg-accent/5 p-8 text-center">
<p className="text-xl font-semibold text-accent mb-2">Inscrição confirmada!</p>
<p className="text-muted-foreground">
Entraremos em contacto em breve. Sem spam 12 atualizações por mês.
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-5">
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label htmlFor="wl-nome" className="block text-sm font-medium mb-1.5">Nome *</label>
<input id="wl-nome" type="text" required value={nome} onChange={(e) => setNome(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
<div>
<label htmlFor="wl-email" className="block text-sm font-medium mb-1.5">Email *</label>
<input id="wl-email" type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
</div>
<div>
<label htmlFor="wl-perfil" className="block text-sm font-medium mb-1.5">Perfil *</label>
<select id="wl-perfil" required value={perfil} onChange={(e) => setPerfil(e.target.value)}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition">
<option value="">Selecionar...</option>
<option value="atleta">Atleta</option>
<option value="treinador">Treinador</option>
<option value="clube">Clube</option>
</select>
</div>
<div>
<label htmlFor="wl-distancia" className="block text-sm font-medium mb-1.5">Distância favorita (opcional)</label>
<input id="wl-distancia" type="text" value={distancia} onChange={(e) => setDistancia(e.target.value)}
placeholder="Ex: Meia-maratona, 10K, Trail 50K"
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
<div>
<label htmlFor="wl-objetivo" className="block text-sm font-medium mb-1.5">Objetivo (opcional)</label>
<input id="wl-objetivo" type="text" value={objetivo} onChange={(e) => setObjetivo(e.target.value)}
placeholder="Ex: Melhorar splits na maratona"
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50 transition" />
</div>
{status === "error" && (
<div role="alert" className="text-sm text-red-500 bg-red-500/5 rounded-lg p-3">
Ocorreu um erro. Tenta novamente.
</div>
)}
<button type="submit" disabled={status === "loading"}
className="w-full rounded-xl bg-accent text-white font-semibold py-3 px-6 hover:bg-accent-dark shadow-lg shadow-accent/20 transition-all disabled:opacity-50 cursor-pointer">
{status === "loading" ? "A enviar..." : "Entrar na lista de espera"}
</button>
<p className="text-xs text-muted-foreground text-center">Sem spam. 12 atualizações por mês.</p>
</form>
);
}

View File

@@ -0,0 +1,52 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import DemoRequestForm from "./DemoRequestForm";
import WaitingListForm from "./WaitingListForm";
export const metadata: Metadata = {
title: "Contactos",
description:
"Pede uma demo personalizada ou junta-te à lista de espera. Estamos aqui para ajudar.",
};
export default function ContactosPage() {
return (
<>
<PageHero
title="Contactos"
description="Pede uma demonstração personalizada ou junta-te à lista de espera para seres dos primeiros a experimentar."
/>
<Section>
<div className="grid lg:grid-cols-2 gap-12">
{/* Demo request */}
<div>
<div className="mb-8">
<h2 className="text-2xl font-bold mb-2">Pedir demonstração</h2>
<p className="text-muted-foreground">
Para treinadores, clubes e organizações. Agendamos uma sessão personalizada.
</p>
</div>
<div className="rounded-2xl border border-border bg-card p-6 md:p-8">
<DemoRequestForm />
</div>
</div>
{/* Waiting list */}
<div>
<div className="mb-8">
<h2 className="text-2xl font-bold mb-2">Lista de espera</h2>
<p className="text-muted-foreground">
Para atletas e entusiastas. dos primeiros a experimentar.
</p>
</div>
<div className="rounded-2xl border border-border bg-card p-6 md:p-8">
<WaitingListForm />
</div>
</div>
</div>
</Section>
</>
);
}

84
src/app/faq/page.tsx Normal file
View File

@@ -0,0 +1,84 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import FAQAccordion from "@/components/FAQAccordion";
import CTASection from "@/components/CTASection";
import { PRODUCT_NAME, BRAND_NAME } from "@/lib/constants";
export const metadata: Metadata = {
title: "FAQ",
description: `Perguntas frequentes sobre o ${PRODUCT_NAME}. Tudo o que precisas de saber antes de experimentar.`,
};
const faqItems = [
{
question: `O que é o ${PRODUCT_NAME}?`,
answer: `O ${PRODUCT_NAME} é um par de óculos com display heads-up (HUD) integrado, desenhado especificamente para corrida de alta performance. Mostra métricas como ritmo, potência, cadência e splits diretamente no teu campo de visão.`,
},
{
question: "Preciso de usar sensores externos?",
answer:
"O HUD tem sensores básicos integrados (acelerómetro, giroscópio). Para métricas como frequência cardíaca e potência, recomendamos sensores externos compatíveis via Bluetooth ou ANT+. O dispositivo é compatível com a maioria dos sensores disponíveis no mercado.",
},
{
question: "Qual é a autonomia da bateria?",
answer:
"O módulo HUD tem uma autonomia estimada de mais de 8 horas em utilização contínua. Suficiente para treinos longos e ultramaratonas. O carregamento é feito via USB-C com carga rápida.",
},
{
question: "O display distrai durante a corrida?",
answer:
"O display foi desenhado para ser não-intrusivo. Está posicionado no canto do campo de visão e utiliza um design monocromático minimalista. A informação é visível com um olhar rápido, sem obstruir a visão periférica.",
},
{
question: "Posso usar em condições de chuva ou suor intenso?",
answer:
"Sim. O módulo HUD tem proteção IP54, resistente a suor e chuva leve. É construído para treinos em condições variáveis.",
},
{
question: "Funciona com o meu relógio GPS?",
answer:
"O HUD funciona de forma independente, com os seus próprios sensores e conectividade. Não é um acessório para relógios GPS — é uma alternativa pensada para quem quer dados no campo de visão.",
},
{
question: "Posso personalizar as métricas que aparecem?",
answer:
"Sim. Através da app companion (iOS e Android), podes escolher as métricas, o layout e os alertas que queres ver em cada modo de treino.",
},
{
question: `O ${PRODUCT_NAME} já está à venda?`,
answer: `O ${PRODUCT_NAME} está atualmente em fase de testes com atletas e treinadores. Podes juntar-te à lista de espera para seres dos primeiros a experimentar quando estiver disponível.`,
},
{
question: "Como posso pedir uma demonstração?",
answer: `Visita a nossa página de contactos e preenche o formulário de "Pedir demo". Entraremos em contacto para agendar uma demonstração personalizada.`,
},
{
question: "Os meus dados de treino são privados?",
answer: `Sim. Os dados de treino pertencem ao atleta. Processamos dados localmente no dispositivo e não vendemos dados pessoais a terceiros. Consulta a nossa política de privacidade para mais detalhes.`,
},
];
export default function FAQPage() {
return (
<>
<PageHero
title="Perguntas frequentes"
description="Tudo o que precisas de saber sobre o produto, os dados e como funciona."
/>
<Section>
<div className="max-w-3xl mx-auto">
<FAQAccordion items={faqItems} />
</div>
</Section>
<CTASection
title="Ainda tens dúvidas?"
description="Fala connosco diretamente. Estamos aqui para ajudar."
primaryLabel="Contactar"
primaryHref="/contactos"
/>
</>
);
}

View File

@@ -1,26 +1,88 @@
@import "tailwindcss"; @import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans); --color-accent: var(--accent);
--font-mono: var(--font-geist-mono); --color-accent-light: var(--accent-light);
--color-accent-dark: var(--accent-dark);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
}
:root {
--background: #fafafa;
--foreground: #0a0a0a;
--accent: #0ea5e9;
--accent-light: #38bdf8;
--accent-dark: #0284c7;
--muted: #f4f4f5;
--muted-foreground: #71717a;
--border: #e4e4e7;
--card: #ffffff;
--card-foreground: #0a0a0a;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background: #0a0a0a; --background: #09090b;
--foreground: #ededed; --foreground: #fafafa;
--accent: #38bdf8;
--accent-light: #7dd3fc;
--accent-dark: #0ea5e9;
--muted: #18181b;
--muted-foreground: #a1a1aa;
--border: #27272a;
--card: #18181b;
--card-foreground: #fafafa;
} }
} }
* {
box-sizing: border-box;
}
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background: var(--accent);
color: white;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.6s ease-out both;
}
.animate-fade-in-delay {
animation: fade-in 0.6s ease-out 0.15s both;
}
.animate-fade-in-delay-2 {
animation: fade-in 0.6s ease-out 0.3s both;
} }

View File

@@ -1,20 +1,40 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { BRAND_NAME, SITE_DESCRIPTION, SITE_URL } from "@/lib/constants";
const geistSans = Geist({ const inter = Inter({
variable: "--font-geist-sans", variable: "--font-inter",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"], subsets: ["latin"],
display: "swap",
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", metadataBase: new URL(SITE_URL),
description: "Generated by create next app", title: {
default: `${BRAND_NAME} — Óculos AR para Corrida de Alta Performance`,
template: `%s | ${BRAND_NAME}`,
},
description: SITE_DESCRIPTION,
openGraph: {
title: `${BRAND_NAME} — Óculos AR para Corrida de Alta Performance`,
description: SITE_DESCRIPTION,
url: SITE_URL,
siteName: BRAND_NAME,
locale: "pt_PT",
type: "website",
},
twitter: {
card: "summary_large_image",
title: `${BRAND_NAME} — Óculos AR para Corrida de Alta Performance`,
description: SITE_DESCRIPTION,
},
robots: {
index: true,
follow: true,
},
}; };
export default function RootLayout({ export default function RootLayout({
@@ -23,11 +43,11 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="pt-PT">
<body <body className={`${inter.variable} font-sans antialiased`}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`} <Header />
> <main className="min-h-screen">{children}</main>
{children} <Footer />
</body> </body>
</html> </html>
); );

View File

@@ -1,65 +1,255 @@
import Image from "next/image"; import type { Metadata } from "next";
import Section from "@/components/Section";
import Container from "@/components/Container";
import Button from "@/components/Button";
import Card from "@/components/Card";
import HUDPreviewSVG from "@/components/HUDPreviewSVG";
import TestimonialPlaceholder from "@/components/TestimonialPlaceholder";
import { BRAND_NAME, PRODUCT_NAME } from "@/lib/constants";
import LeadCaptureForm from "./LeadCaptureForm";
export default function Home() { export const metadata: Metadata = {
title: `${BRAND_NAME} — Ritmo, potência e feedback em tempo real`,
description:
"Óculos AR para atletas e treinadores focados em alta competição. Métricas no campo de visão, sem desviar os olhos do treino.",
};
export default function HomePage() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <>
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> {/* ─── Hero ─── */}
<Image <section className="pt-20 pb-16 md:pt-32 md:pb-24">
className="dark:invert" <Container>
src="/next.svg" <div className="grid lg:grid-cols-2 gap-12 items-center">
alt="Next.js logo" <div>
width={100} <h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-[1.1] animate-fade-in">
height={20} Ritmo, potência e feedback em tempo real
priority <span className="text-accent"> sem tirares os olhos do treino.</span>
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1> </h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> <p className="mt-6 text-lg md:text-xl text-muted-foreground leading-relaxed animate-fade-in-delay max-w-xl">
Looking for a starting point or more instructions? Head over to{" "} Óculos AR para atletas e treinadores focados em alta competição.
<a </p>
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <div className="mt-8 flex flex-col sm:flex-row gap-4 animate-fade-in-delay-2">
className="font-medium text-zinc-950 dark:text-zinc-50" <Button href="/contactos" trackEvent="hero_pedir_demo" size="lg">
> Pedir demo
Templates </Button>
</a>{" "} <Button href="#video" variant="secondary" size="lg" trackEvent="hero_ver_como_funciona">
or the{" "} Ver como funciona
<a </Button>
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" </div>
className="font-medium text-zinc-950 dark:text-zinc-50" </div>
> <div className="flex justify-center animate-fade-in-delay">
Learning <div className="w-full max-w-lg rounded-2xl border border-border bg-card p-6 shadow-xl">
</a>{" "} <HUDPreviewSVG variant="pace" />
center. </div>
</div>
</div>
</Container>
</section>
{/* ─── Proof columns ─── */}
<Section muted>
<div className="grid md:grid-cols-3 gap-8">
{[
{
icon: (
<svg className="h-8 w-8 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6l4 2m6-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
title: "Pacing de competição",
desc: "Ritmo-alvo, delta e splits — sempre visíveis, sem desviar os olhos da estrada.",
},
{
icon: (
<svg className="h-8 w-8 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg>
),
title: "Eficiência e técnica",
desc: "Cadência, potência e oscilação vertical — métricas biomecânicas em tempo real.",
},
{
icon: (
<svg className="h-8 w-8 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
),
title: "Treinos guiados",
desc: "Intervalos, tempo runs e sessões estruturadas com instruções visuais no HUD.",
},
].map((item, i) => (
<Card key={i}>
<div className="mb-4">{item.icon}</div>
<h3 className="text-xl font-bold mb-2">{item.title}</h3>
<p className="text-muted-foreground leading-relaxed">
{item.desc}
</p>
</Card>
))}
</div>
</Section>
{/* ─── Video placeholder ─── */}
<Section id="video">
<div className="text-center mb-10">
<h2 className="text-3xl md:text-4xl font-bold">
o {PRODUCT_NAME} em ação
</h2>
<p className="mt-4 text-lg text-muted-foreground">
Uma experiência pensada para não interromper o teu fluxo.
</p> </p>
</div> </div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row"> <div className="relative mx-auto max-w-4xl aspect-video rounded-2xl bg-muted border border-border overflow-hidden flex items-center justify-center">
<a {/* Poster frame */}
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]" <div className="absolute inset-0 bg-gradient-to-br from-accent/5 to-transparent" />
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <button
target="_blank" className="relative z-10 flex h-20 w-20 items-center justify-center rounded-full bg-accent text-white shadow-2xl shadow-accent/30 hover:scale-105 transition-transform cursor-pointer"
rel="noopener noreferrer" aria-label="Reproduzir vídeo demonstrativo"
> >
<Image <svg className="h-8 w-8 ml-1" fill="currentColor" viewBox="0 0 24 24">
className="dark:invert" <path d="M8 5v14l11-7z" />
src="/vercel.svg" </svg>
alt="Vercel logomark" </button>
width={16} <p className="absolute bottom-6 text-sm text-muted-foreground">
height={16} Demonstração vídeo em breve
/> </p>
Deploy Now </div>
</a> </Section>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" {/* ─── Before vs After ─── */}
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <Section muted>
target="_blank" <h2 className="text-3xl md:text-4xl font-bold text-center mb-12">
rel="noopener noreferrer" Antes vs. depois
</h2>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{/* Before */}
<div className="rounded-2xl border border-border bg-card p-8">
<div className="flex items-center gap-3 mb-6">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-red-500/10">
<svg className="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h3 className="text-xl font-bold">Relógio / Telemóvel</h3>
</div>
<ul className="space-y-3">
{[
"Olhar para o pulso distrai e quebra a postura",
"Dados pequenos, difíceis de ler em movimento",
"Interação manual durante o esforço",
"Feedback atrasado — reação tardia",
].map((item, i) => (
<li key={i} className="flex items-start gap-3 text-muted-foreground">
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-red-400 shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* After */}
<div className="rounded-2xl border-2 border-accent/30 bg-card p-8 shadow-lg shadow-accent/5">
<div className="flex items-center gap-3 mb-6">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-accent/10">
<svg className="h-5 w-5 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 className="text-xl font-bold">HUD no campo de visão</h3>
</div>
<ul className="space-y-3">
{[
"Métricas sempre visíveis sem desviar o olhar",
"Display claro e legível em qualquer condição",
"Zero interação manual — feedback automático",
"Dados em tempo real — reação imediata",
].map((item, i) => (
<li key={i} className="flex items-start gap-3 text-muted-foreground">
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-accent shrink-0" />
{item}
</li>
))}
</ul>
</div>
</div>
</Section>
{/* ─── For whom ─── */}
<Section>
<h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
Para quem é o {PRODUCT_NAME}?
</h2>
<p className="text-center text-lg text-muted-foreground mb-12 max-w-2xl mx-auto">
Desenhado para quem procura a diferença entre bom e excelente.
</p>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<Card>
<h3 className="text-2xl font-bold mb-3">Atletas</h3>
<p className="text-muted-foreground mb-4">
Corredores de pista, estrada e trail que precisam de dados precisos sem comprometer o foco.
</p>
<div className="flex flex-wrap gap-2">
{["Pista", "Estrada", "Trail"].map((tag) => (
<span
key={tag}
className="px-3 py-1 text-xs font-semibold rounded-full bg-accent/10 text-accent"
> >
Documentation {tag}
</a> </span>
))}
</div> </div>
</main> </Card>
<Card>
<h3 className="text-2xl font-bold mb-3">Treinadores e Clubes</h3>
<p className="text-muted-foreground mb-4">
Monitorização de múltiplos atletas, feedback remoto e estruturação de treinos guiados.
</p>
<div className="flex flex-wrap gap-2">
{["Multi-atleta", "Feedback remoto", "Sessões guiadas"].map((tag) => (
<span
key={tag}
className="px-3 py-1 text-xs font-semibold rounded-full bg-accent/10 text-accent"
>
{tag}
</span>
))}
</div> </div>
</Card>
</div>
</Section>
{/* ─── Credibility ─── */}
<Section muted>
<div className="max-w-3xl mx-auto">
<TestimonialPlaceholder />
<div className="mt-8 grid sm:grid-cols-3 gap-6 text-center">
{[
{ value: "< 30g", label: "peso do módulo HUD" },
{ value: "8h+", label: "autonomia estimada" },
{ value: "< 2s", label: "latência de dados" },
].map((stat, i) => (
<div key={i} className="p-4">
<p className="text-3xl font-bold text-accent">{stat.value}</p>
<p className="text-sm text-muted-foreground mt-1">{stat.label}</p>
</div>
))}
</div>
</div>
</Section>
{/* ─── Lead capture CTA ─── */}
<Section>
<div className="max-w-xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold mb-4">
dos primeiros a experimentar
</h2>
<p className="text-lg text-muted-foreground mb-8">
Sem spam. 12 atualizações por mês.
</p>
<LeadCaptureForm />
</div>
</Section>
</>
); );
} }

View File

@@ -0,0 +1,138 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import Card from "@/components/Card";
import HUDPreviewSVG from "@/components/HUDPreviewSVG";
import CTASection from "@/components/CTASection";
import Button from "@/components/Button";
export const metadata: Metadata = {
title: "Performance e Métricas",
description:
"Métricas de corrida em tempo real no campo de visão: pace, potência, cadência, splits e mais.",
};
export default function PerformancePage() {
return (
<>
<PageHero
title="Performance e métricas"
description="Dados precisos, em tempo real, sem desviar os olhos do treino. Cada métrica no sítio certo, no momento certo."
>
<Button href="/contactos" trackEvent="performance_pedir_demo">
Pedir demo
</Button>
</PageHero>
<Section muted>
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
Métricas disponíveis
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
metric: "Ritmo (Pace)",
desc: "Min/km em tempo real com comparação face ao alvo. Resposta imediata a mudanças de velocidade.",
unit: "min/km",
},
{
metric: "Potência",
desc: "Watts instantâneos e médios. Gestão de esforço independente de vento, inclinação ou terreno.",
unit: "W",
},
{
metric: "Frequência cardíaca",
desc: "BPM atuais e zona de treino. Deteção de cardiac drift para gestão de fadiga.",
unit: "bpm",
},
{
metric: "Cadência",
desc: "Passos por minuto com indicador de zona ideal. Feedback para otimização biomecânica.",
unit: "spm",
},
{
metric: "Splits",
desc: "Tempos parciais automáticos por quilómetro. Comparação direta com o plano de prova.",
unit: "auto",
},
{
metric: "Distância e elevação",
desc: "Distância acumulada e desnível positivo/negativo. Essencial para treinos de trail.",
unit: "km / m",
},
].map((item, i) => (
<Card key={i}>
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-bold">{item.metric}</h3>
<span className="text-xs font-mono text-accent bg-accent/10 px-2 py-1 rounded">
{item.unit}
</span>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
{item.desc}
</p>
</Card>
))}
</div>
</Section>
<Section>
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">
Visualização no HUD
</h2>
<p className="text-center text-muted-foreground mb-12 max-w-2xl mx-auto">
Cada modo é otimizado para um contexto diferente. Muda entre eles com
um gesto simples ou deixa o sistema adaptar-se automaticamente.
</p>
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-4">
<HUDPreviewSVG variant="pace" />
<p className="text-sm font-semibold text-center">Modo Pacing</p>
</div>
<div className="space-y-4">
<HUDPreviewSVG variant="cadence" />
<p className="text-sm font-semibold text-center">Modo Cadência</p>
</div>
</div>
</Section>
<Section muted>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">
Porque importa
</h2>
<div className="space-y-6">
{[
{
q: "Feedback instantâneo",
a: "Dados com latência inferior a 2 segundos. A reação ao esforço é imediata, ao contrário do GPS que pode demorar 510 segundos.",
},
{
q: "Menos carga cognitiva",
a: "Sem necessidade de memorizar dados ou fazer cálculos mentais. O HUD processa e apresenta a informação relevante.",
},
{
q: "Foco no treino",
a: "Sem desviar os olhos, sem virar o pulso, sem perder a postura. O feedback está onde o teu olhar já está.",
},
].map((item, i) => (
<div key={i} className="flex gap-4">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent/10 text-accent font-bold text-sm">
{i + 1}
</div>
<div>
<h3 className="font-bold mb-1">{item.q}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{item.a}
</p>
</div>
</div>
))}
</div>
</div>
</Section>
<CTASection />
</>
);
}

View File

@@ -0,0 +1,81 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import { BRAND_NAME, PRODUCT_NAME } from "@/lib/constants";
export const metadata: Metadata = {
title: "Privacidade",
description: `Política de privacidade do ${BRAND_NAME}. Como protegemos os teus dados.`,
};
export default function PrivacidadePage() {
return (
<>
<PageHero
title="Privacidade e proteção de dados"
description="A tua performance é tua. Sabes exatamente que dados recolhemos, porquê e como os protegemos."
/>
<Section>
<div className="max-w-3xl mx-auto prose prose-neutral dark:prose-invert">
<h2 id="principios" className="text-2xl font-bold mb-4">Princípios</h2>
<ul className="space-y-2 text-muted-foreground mb-8 list-disc pl-6">
<li>Recolhemos apenas os dados necessários para o funcionamento do produto.</li>
<li>Não vendemos dados pessoais a terceiros.</li>
<li>Os dados de treino pertencem ao atleta.</li>
<li>Transparência total sobre o que é recolhido e processado.</li>
</ul>
<h2 className="text-2xl font-bold mb-4">Dados recolhidos pelo {PRODUCT_NAME}</h2>
<p className="text-muted-foreground mb-4">
Durante a utilização do HUD, os seguintes dados podem ser recolhidos e processados localmente:
</p>
<ul className="space-y-2 text-muted-foreground mb-8 list-disc pl-6">
<li><strong>Métricas de treino:</strong> ritmo, distância, cadência, potência, frequência cardíaca (via sensores externos).</li>
<li><strong>Dados de localização:</strong> percurso GPS durante o treino (processado localmente, sincronizado opcionalmente).</li>
<li><strong>Dados de dispositivo:</strong> versão de firmware, estado da bateria, diagnósticos de conectividade.</li>
</ul>
<h2 className="text-2xl font-bold mb-4">Dados recolhidos pelo website</h2>
<p className="text-muted-foreground mb-4">
Ao utilizar este website e submeter formulários, recolhemos:
</p>
<ul className="space-y-2 text-muted-foreground mb-8 list-disc pl-6">
<li><strong>Formulários:</strong> nome, email, perfil e informação voluntariamente fornecida.</li>
<li><strong>Analítica:</strong> dados agregados de utilização (páginas visitadas, cliques). Não utilizamos cookies de terceiros para rastreamento.</li>
</ul>
<h2 id="termos" className="text-2xl font-bold mb-4 pt-8 border-t border-border">Termos de uso</h2>
<p className="text-muted-foreground mb-4">
Ao utilizar este website e os nossos produtos, aceita os seguintes termos:
</p>
<ul className="space-y-2 text-muted-foreground mb-8 list-disc pl-6">
<li>O {PRODUCT_NAME} é um dispositivo de apoio ao treino, não um dispositivo médico.</li>
<li>As métricas fornecidas são indicativas e não substituem avaliação profissional.</li>
<li>O utilizador é responsável pela utilização segura do dispositivo durante a atividade desportiva.</li>
<li>A {BRAND_NAME} reserva-se o direito de atualizar estes termos, notificando os utilizadores registados.</li>
</ul>
<h2 id="cookies" className="text-2xl font-bold mb-4 pt-8 border-t border-border">Cookies</h2>
<p className="text-muted-foreground mb-4">
Este website utiliza cookies estritamente necessários para o funcionamento básico:
</p>
<ul className="space-y-2 text-muted-foreground mb-8 list-disc pl-6">
<li><strong>Cookies essenciais:</strong> necessários para navegação e funcionalidades básicas.</li>
<li><strong>Cookies de preferências:</strong> tema (claro/escuro), idioma.</li>
<li>Não utilizamos cookies de publicidade ou rastreamento de terceiros.</li>
</ul>
<h2 className="text-2xl font-bold mb-4 pt-8 border-t border-border">Contacto</h2>
<p className="text-muted-foreground">
Para questões relacionadas com privacidade e proteção de dados, contacte-nos através da página de{" "}
<a href="/contactos" className="text-accent hover:text-accent-dark">
contactos
</a>
.
</p>
</div>
</Section>
</>
);
}

93
src/app/produto/page.tsx Normal file
View File

@@ -0,0 +1,93 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import Card from "@/components/Card";
import HUDPreviewSVG from "@/components/HUDPreviewSVG";
import CTASection from "@/components/CTASection";
import Button from "@/components/Button";
import { PRODUCT_NAME } from "@/lib/constants";
export const metadata: Metadata = {
title: "Produto",
description: `Descobre o ${PRODUCT_NAME}: óculos AR com HUD minimalista para métricas de corrida em tempo real.`,
};
export default function ProdutoPage() {
return (
<>
<PageHero
title={`Conhece o ${PRODUCT_NAME}`}
description="Um display heads-up desenhado especificamente para corrida de alta performance. Leve, discreto e construído para não interromper o teu fluxo."
>
<Button href="/contactos" trackEvent="produto_pedir_demo">
Pedir demo
</Button>
</PageHero>
<Section muted>
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
O que vês no HUD
</h2>
<div className="grid md:grid-cols-2 gap-10">
<div>
<HUDPreviewSVG variant="pace" />
<h3 className="text-xl font-bold mt-6 mb-2">Modo Pacing</h3>
<p className="text-muted-foreground">
Ritmo atual, ritmo-alvo, delta e splits. Tudo o que precisas para executar uma estratégia de prova perfeita.
</p>
</div>
<div>
<HUDPreviewSVG variant="power" />
<h3 className="text-xl font-bold mt-6 mb-2">Modo Potência</h3>
<p className="text-muted-foreground">
Watts instantâneos, zona de treino e potência média. Gestão de esforço baseada em dados objetivos.
</p>
</div>
</div>
</Section>
<Section>
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
Características principais
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
title: "Display não-intrusivo",
desc: "HUD monocromático posicionado no canto do campo de visão. Vês os dados sem perder foco na estrada.",
},
{
title: "Ultra-leve",
desc: "Módulo HUD com menos de 30g. Desenhado para ser esquecido durante o uso.",
},
{
title: "Resistente",
desc: "Proteção contra suor, chuva e poeira. Construído para treinos em qualquer condição.",
},
{
title: "Autonomia prolongada",
desc: "Mais de 8 horas de utilização contínua. Suficiente para ultramaratonas.",
},
{
title: "Compatível com sensores",
desc: "Ligação a sensores de frequência cardíaca, potência e cadência via Bluetooth/ANT+.",
},
{
title: "Personalizável",
desc: "Escolhe as métricas que queres ver, o layout e os alertas. Configuração via app companion.",
},
].map((feature, i) => (
<Card key={i}>
<h3 className="text-lg font-bold mb-2">{feature.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{feature.desc}
</p>
</Card>
))}
</div>
</Section>
<CTASection />
</>
);
}

15
src/app/robots.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/constants";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/"],
},
],
sitemap: `${SITE_URL}/sitemap.xml`,
};
}

37
src/app/sitemap.ts Normal file
View File

@@ -0,0 +1,37 @@
import type { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/blog-data";
import { SITE_URL } from "@/lib/constants";
export default function sitemap(): MetadataRoute.Sitemap {
const staticRoutes = [
"",
"/produto",
"/performance",
"/treinos-guiados",
"/treinadores-e-clubes",
"/casos-de-uso",
"/tecnologia",
"/privacidade",
"/faq",
"/blog",
"/sobre",
"/contactos",
];
const blogRoutes = getAllPosts().map((post) => ({
url: `${SITE_URL}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "monthly" as const,
priority: 0.6,
}));
return [
...staticRoutes.map((route) => ({
url: `${SITE_URL}${route}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: route === "" ? 1 : 0.8,
})),
...blogRoutes,
];
}

94
src/app/sobre/page.tsx Normal file
View File

@@ -0,0 +1,94 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import CTASection from "@/components/CTASection";
import { BRAND_NAME, PRODUCT_NAME } from "@/lib/constants";
export const metadata: Metadata = {
title: "Sobre nós",
description: `Conhece a equipa por detrás do ${BRAND_NAME}. Missão, visão e valores.`,
};
export default function SobrePage() {
return (
<>
<PageHero
title="Sobre nós"
description="Uma equipa obcecada com performance, dados e a experiência do atleta."
/>
<Section>
<div className="max-w-3xl mx-auto space-y-12">
<div>
<h2 className="text-2xl font-bold mb-4">Missão</h2>
<p className="text-muted-foreground leading-relaxed text-lg">
Acreditamos que os melhores atletas tomam as melhores decisões e que as melhores decisões vêm de informação clara, no momento certo. A nossa missão é colocar os dados de treino onde pertencem: no campo de visão do atleta, sem comprometer o foco.
</p>
</div>
<div>
<h2 className="text-2xl font-bold mb-4">O problema que resolvemos</h2>
<p className="text-muted-foreground leading-relaxed">
Cada vez que um corredor olha para o relógio, perde 12 segundos de foco, altera a postura e interrompe o fluxo natural da corrida. Numa prova de 42 km com verificações a cada quilómetro, são minutos de distração acumulada.
</p>
<p className="text-muted-foreground leading-relaxed mt-4">
O {PRODUCT_NAME} elimina esse problema. As métricas estão sempre visíveis, sem gestos, sem pausas, sem desvio do olhar.
</p>
</div>
<div>
<h2 className="text-2xl font-bold mb-4">Valores</h2>
<div className="grid sm:grid-cols-2 gap-6">
{[
{
title: "Foco no atleta",
desc: "Cada decisão de design começa com a pergunta: isto ajuda o atleta a correr melhor?",
},
{
title: "Dados, não ruído",
desc: "Menos informação, mais relevante. Alertas mínimos, impacto máximo.",
},
{
title: "Transparência",
desc: "Somos claros sobre o que o produto faz, o que não faz, e o que está em desenvolvimento.",
},
{
title: "Performance real",
desc: "Não prometemos magia. Oferecemos dados precisos para decisões mais inteligentes.",
},
].map((value, i) => (
<div key={i} className="p-5 rounded-xl border border-border">
<h3 className="font-bold mb-2">{value.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{value.desc}
</p>
</div>
))}
</div>
</div>
<div>
<h2 className="text-2xl font-bold mb-4">Fase atual</h2>
<div className="rounded-xl border border-accent/30 bg-accent/5 p-6">
<p className="text-muted-foreground leading-relaxed">
O {PRODUCT_NAME} está atualmente <strong className="text-foreground">em fase de testes com atletas e treinadores</strong>. Estamos a refinar o hardware, o software e a experiência de utilização com base em feedback real de corredores de competição.
</p>
<p className="text-muted-foreground leading-relaxed mt-3">
Se és atleta ou treinador e queres participar na fase de testes, entra em contacto connosco.
</p>
</div>
</div>
</div>
</Section>
<CTASection
title="Queres fazer parte?"
description="Junta-te à fase de testes ou fica na lista de espera para ser dos primeiros a experimentar."
primaryLabel="Contactar"
primaryHref="/contactos"
secondaryLabel="Lista de espera"
secondaryHref="/contactos"
/>
</>
);
}

131
src/app/tecnologia/page.tsx Normal file
View File

@@ -0,0 +1,131 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import Card from "@/components/Card";
import CTASection from "@/components/CTASection";
import Button from "@/components/Button";
import { PRODUCT_NAME } from "@/lib/constants";
export const metadata: Metadata = {
title: "Tecnologia",
description: `A tecnologia por detrás do ${PRODUCT_NAME}: display óptico, sensores, conectividade e processamento em tempo real.`,
};
export default function TecnologiaPage() {
return (
<>
<PageHero
title="Tecnologia"
description="Engenharia pensada para performance. Cada componente foi desenhado para ser rápido, leve e fiável."
>
<Button href="/contactos" trackEvent="tecnologia_pedir_demo">
Pedir demo
</Button>
</PageHero>
<Section muted>
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
Arquitectura do sistema
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{
title: "Display óptico",
specs: ["Resolução otimizada", "Visível em luz solar", "Baixo consumo"],
desc: "Micro-display posicionado no canto do campo de visão. Informação legível sem obstruir a visão periférica.",
},
{
title: "Processamento",
specs: ["CPU low-power", "< 2s latência", "Edge computing"],
desc: "Processamento local para latência mínima. Sem dependência de rede durante o treino.",
},
{
title: "Sensores",
specs: ["Acelerómetro", "Giroscópio", "GPS assistido"],
desc: "Sensores integrados para métricas básicas. Compatível com sensores externos para precisão máxima.",
},
{
title: "Conectividade",
specs: ["Bluetooth 5.3", "ANT+", "Wi-Fi sync"],
desc: "Ligação a sensores de FC, potência e cadência. Sincronização via Wi-Fi após o treino.",
},
].map((item, i) => (
<Card key={i}>
<h3 className="text-lg font-bold mb-3">{item.title}</h3>
<div className="flex flex-wrap gap-1.5 mb-3">
{item.specs.map((spec) => (
<span
key={spec}
className="text-xs font-mono px-2 py-1 rounded bg-accent/5 text-accent"
>
{spec}
</span>
))}
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
{item.desc}
</p>
</Card>
))}
</div>
</Section>
<Section>
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
Especificações técnicas
</h2>
<div className="max-w-2xl mx-auto">
<div className="rounded-2xl border border-border overflow-hidden">
{[
{ label: "Peso do módulo HUD", value: "< 30g" },
{ label: "Autonomia", value: "8+ horas" },
{ label: "Latência de dados", value: "< 2 segundos" },
{ label: "Resistência", value: "IP54 (suor, chuva leve)" },
{ label: "Conectividade", value: "Bluetooth 5.3 + ANT+" },
{ label: "Carregamento", value: "USB-C, carga rápida" },
{ label: "App companion", value: "iOS e Android" },
{ label: "Atualizações", value: "OTA (over-the-air)" },
].map((spec, i) => (
<div
key={i}
className={`flex justify-between px-6 py-4 ${i % 2 === 0 ? "bg-card" : "bg-muted/50"
}`}
>
<span className="text-sm font-medium">{spec.label}</span>
<span className="text-sm font-mono text-accent">
{spec.value}
</span>
</div>
))}
</div>
</div>
</Section>
<Section muted>
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">
Integrações (em desenvolvimento)
</h2>
<p className="text-center text-muted-foreground mb-8 max-w-2xl mx-auto">
Estamos a trabalhar para garantir compatibilidade com as plataformas mais utilizadas por atletas e treinadores.
</p>
<div className="flex flex-wrap justify-center gap-4">
{[
"Compatível com sensores Bluetooth/ANT+",
"Exportação em formato .FIT",
"API aberta (em desenvolvimento)",
"Sincronização com plataformas de treino",
].map((item) => (
<div
key={item}
className="px-5 py-3 rounded-xl border border-border bg-card text-sm text-muted-foreground"
>
{item}
</div>
))}
</div>
</Section>
<CTASection />
</>
);
}

View File

@@ -0,0 +1,117 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import Card from "@/components/Card";
import CTASection from "@/components/CTASection";
import Button from "@/components/Button";
export const metadata: Metadata = {
title: "Treinadores e Clubes",
description:
"Monitoriza múltiplos atletas em tempo real, envia feedback remoto e estrutura treinos guiados para a equipa.",
};
export default function TreinadoresPage() {
return (
<>
<PageHero
title="Para treinadores e clubes"
description="Monitorização multi-atleta, feedback remoto e treinos estruturados. Tudo o que precisas para elevar a performance da tua equipa."
>
<Button href="/contactos" trackEvent="treinadores_pedir_demo">
Pedir demo
</Button>
</PageHero>
<Section muted>
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
O que muda para ti
</h2>
<div className="grid md:grid-cols-3 gap-8">
{[
{
icon: (
<svg className="h-8 w-8 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
),
title: "Dashboard multi-atleta",
desc: "Visualiza os dados de todos os teus atletas num único painel. Métricas em tempo real durante treinos e provas.",
},
{
icon: (
<svg className="h-8 w-8 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" />
</svg>
),
title: "Feedback remoto",
desc: "Envia mensagens e instruções diretamente para o HUD do atleta durante o treino. Comunicação sem interrupção.",
},
{
icon: (
<svg className="h-8 w-8 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
title: "Sessões programáveis",
desc: "Cria treinos estruturados e distribui-os pela equipa. Cada atleta recebe a sessão no seu HUD.",
},
].map((item, i) => (
<Card key={i}>
<div className="mb-4">{item.icon}</div>
<h3 className="text-xl font-bold mb-2">{item.title}</h3>
<p className="text-muted-foreground leading-relaxed">{item.desc}</p>
</Card>
))}
</div>
</Section>
<Section>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">
Cenários de utilização
</h2>
<div className="space-y-6">
{[
{
scenario: "Treino em grupo na pista",
desc: "Cada atleta tem o seu plano de intervalos personalizado no HUD. O treinador monitoriza todos em tempo real e envia ajustes individuais.",
},
{
scenario: "Preparação de prova",
desc: "Defina os splits-alvo por quilómetro e envie-os para o HUD do atleta. Durante a prova, acompanhe a execução em tempo real.",
},
{
scenario: "Treino à distância",
desc: "Atletas em diferentes locais executam a sessão com feedback do HUD. O treinador revê os dados após o treino.",
},
{
scenario: "Avaliação e triagem",
desc: "Use métricas objetivas — cadência, potência, cardiac drift — para avaliar a forma do atleta e ajustar cargas de treino.",
},
].map((item, i) => (
<div key={i} className="flex gap-4 p-6 rounded-xl border border-border hover:border-accent/20 transition-colors">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/10 text-accent text-sm font-bold">
{i + 1}
</div>
<div>
<h3 className="font-bold mb-1">{item.scenario}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{item.desc}
</p>
</div>
</div>
))}
</div>
</div>
</Section>
<CTASection
title="Quer experimentar com a sua equipa?"
description="Peça uma demo personalizada e descubra como o HUD pode transformar os treinos do seu grupo."
primaryLabel="Pedir demo"
/>
</>
);
}

View File

@@ -0,0 +1,133 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import Section from "@/components/Section";
import Card from "@/components/Card";
import HUDPreviewSVG from "@/components/HUDPreviewSVG";
import CTASection from "@/components/CTASection";
import Button from "@/components/Button";
export const metadata: Metadata = {
title: "Treinos Guiados",
description:
"Sessões de treino estruturadas com instruções visuais no HUD. Intervalos, tempo runs e progressões — sem precisar de decorar o plano.",
};
export default function TreinosGuiadosPage() {
return (
<>
<PageHero
title="Treinos guiados"
description="Sessões estruturadas com instruções visuais diretas no HUD. O teu plano de treino, sempre visível."
>
<Button href="/contactos" trackEvent="treinos_pedir_demo">
Pedir demo
</Button>
</PageHero>
<Section muted>
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-center">
Como funciona
</h2>
<p className="text-center text-muted-foreground mb-12 max-w-2xl mx-auto">
Programa a sessão na app, coloca os óculos e corre. O HUD guia-te passo a passo.
</p>
<div className="grid md:grid-cols-3 gap-6">
{[
{
step: "01",
title: "Configura",
desc: "Define a sessão na app companion: tipo de treino, intervalos, ritmos-alvo e recuperações.",
},
{
step: "02",
title: "Executa",
desc: "O HUD mostra a instrução atual — tempo restante, ritmo-alvo, número do intervalo. Sem distrações.",
},
{
step: "03",
title: "Analisa",
desc: "Após o treino, revisita os dados: cumprimento de objetivos, desvios e progressão ao longo do tempo.",
},
].map((item, i) => (
<Card key={i}>
<span className="text-4xl font-bold text-accent/20">{item.step}</span>
<h3 className="text-xl font-bold mt-3 mb-2">{item.title}</h3>
<p className="text-muted-foreground leading-relaxed">{item.desc}</p>
</Card>
))}
</div>
</Section>
<Section>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl md:text-4xl font-bold mb-6">
Intervalos na pista
</h2>
<p className="text-muted-foreground leading-relaxed mb-6">
O HUD mostra o intervalo atual, tempo restante, ritmo instantâneo e a próxima recuperação. Sabes exatamente o que fazer em cada momento sem pausar, sem consultar.
</p>
<ul className="space-y-3">
{[
"Contagem regressiva visual por intervalo",
"Ritmo atual vs. ritmo-alvo",
"Indicação automática de recuperação",
"Resumo de cada intervalo ao terminar",
].map((item, i) => (
<li key={i} className="flex items-start gap-3 text-sm text-muted-foreground">
<svg className="h-5 w-5 text-accent shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
{item}
</li>
))}
</ul>
</div>
<div>
<HUDPreviewSVG variant="interval" />
</div>
</div>
</Section>
<Section muted>
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-center">
Tipos de sessão
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{
title: "Intervalos",
desc: "400m a 2000m com recuperação programada.",
example: "6×800m @ pace 3K",
},
{
title: "Tempo run",
desc: "Ritmo sustentado por tempo ou distância.",
example: "20 min @ pace 10K",
},
{
title: "Progressão",
desc: "Aumento gradual de intensidade ao longo da sessão.",
example: "5K: 5:00 → 4:30 → 4:00/km",
},
{
title: "Fartlek",
desc: "Alternâncias de ritmo com indicações visuais.",
example: "3 min forte / 2 min leve ×6",
},
].map((item, i) => (
<Card key={i}>
<h3 className="font-bold mb-2">{item.title}</h3>
<p className="text-sm text-muted-foreground mb-3">{item.desc}</p>
<div className="text-xs font-mono text-accent bg-accent/5 px-3 py-2 rounded-lg">
{item.example}
</div>
</Card>
))}
</div>
</Section>
<CTASection />
</>
);
}

62
src/components/Button.tsx Normal file
View File

@@ -0,0 +1,62 @@
"use client";
import React from "react";
import { track } from "@/lib/analytics";
type ButtonVariant = "primary" | "secondary" | "ghost";
type ButtonSize = "sm" | "md" | "lg";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
href?: string;
trackEvent?: string;
children: React.ReactNode;
}
const variantClasses: Record<ButtonVariant, string> = {
primary:
"bg-accent text-white hover:bg-accent-dark shadow-lg shadow-accent/20 hover:shadow-accent/30",
secondary:
"border-2 border-accent text-accent hover:bg-accent hover:text-white",
ghost:
"text-muted-foreground hover:text-foreground hover:bg-muted",
};
const sizeClasses: Record<ButtonSize, string> = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
export default function Button({
variant = "primary",
size = "md",
href,
trackEvent,
children,
className = "",
onClick,
...props
}: ButtonProps) {
const classes = `inline-flex items-center justify-center gap-2 font-semibold rounded-xl transition-all duration-200 cursor-pointer focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`;
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trackEvent) track(trackEvent);
onClick?.(e);
};
if (href) {
return (
<a href={href} className={classes} role="button">
{children}
</a>
);
}
return (
<button className={classes} onClick={handleClick} {...props}>
{children}
</button>
);
}

View File

@@ -0,0 +1,42 @@
import React from "react";
import Section from "./Section";
import Button from "./Button";
interface CTASectionProps {
title?: string;
description?: string;
primaryLabel?: string;
primaryHref?: string;
secondaryLabel?: string;
secondaryHref?: string;
muted?: boolean;
}
export default function CTASection({
title = "Pronto para redefinir o teu treino?",
description = "Junta-te à lista de espera e sê dos primeiros a experimentar o futuro da corrida de alta performance.",
primaryLabel = "Pedir demo",
primaryHref = "/contactos",
secondaryLabel,
secondaryHref,
muted = true,
}: CTASectionProps) {
return (
<Section muted={muted}>
<div className="text-center max-w-2xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold mb-4">{title}</h2>
<p className="text-lg text-muted-foreground mb-8">{description}</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button href={primaryHref} trackEvent="cta_pedir_demo">
{primaryLabel}
</Button>
{secondaryLabel && secondaryHref && (
<Button href={secondaryHref} variant="secondary">
{secondaryLabel}
</Button>
)}
</div>
</div>
</Section>
);
}

24
src/components/Card.tsx Normal file
View File

@@ -0,0 +1,24 @@
import React from "react";
interface CardProps {
children: React.ReactNode;
className?: string;
hover?: boolean;
}
export default function Card({
children,
className = "",
hover = true,
}: CardProps) {
return (
<div
className={`rounded-2xl border border-border bg-card p-6 md:p-8 ${hover
? "transition-all duration-200 hover:shadow-lg hover:border-accent/30 hover:-translate-y-1"
: ""
} ${className}`}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,19 @@
import React from "react";
interface ContainerProps {
children: React.ReactNode;
className?: string;
as?: React.ElementType;
}
export default function Container({
children,
className = "",
as: Tag = "div",
}: ContainerProps) {
return (
<Tag className={`mx-auto w-full max-w-6xl px-5 sm:px-8 ${className}`}>
{children}
</Tag>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import React, { useState } from "react";
interface FAQItem {
question: string;
answer: string;
}
interface FAQAccordionProps {
items: FAQItem[];
className?: string;
}
export default function FAQAccordion({
items,
className = "",
}: FAQAccordionProps) {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<div className={`space-y-3 ${className}`}>
{items.map((item, i) => {
const isOpen = openIndex === i;
return (
<div
key={i}
className="rounded-xl border border-border overflow-hidden transition-colors"
>
<button
onClick={() => setOpenIndex(isOpen ? null : i)}
aria-expanded={isOpen}
aria-controls={`faq-panel-${i}`}
id={`faq-header-${i}`}
className="flex w-full items-center justify-between gap-4 px-6 py-5 text-left font-semibold text-foreground hover:bg-muted/50 transition-colors cursor-pointer"
>
<span>{item.question}</span>
<svg
className={`h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 ${isOpen ? "rotate-180" : ""
}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div
id={`faq-panel-${i}`}
role="region"
aria-labelledby={`faq-header-${i}`}
className={`overflow-hidden transition-all duration-200 ${isOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
}`}
>
<p className="px-6 pb-5 text-muted-foreground leading-relaxed">
{item.answer}
</p>
</div>
</div>
);
})}
</div>
);
}

128
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,128 @@
import React from "react";
import Link from "next/link";
import { BRAND_NAME } from "@/lib/constants";
const footerLinks = [
{
title: "Produto",
links: [
{ href: "/produto", label: "Funcionalidades" },
{ href: "/performance", label: "Performance" },
{ href: "/treinos-guiados", label: "Treinos guiados" },
{ href: "/tecnologia", label: "Tecnologia" },
{ href: "/casos-de-uso", label: "Casos de uso" },
],
},
{
title: "Recursos",
links: [
{ href: "/blog", label: "Blog" },
{ href: "/faq", label: "FAQ" },
{ href: "/sobre", label: "Sobre nós" },
{ href: "/contactos", label: "Contactos" },
],
},
{
title: "Legal",
links: [
{ href: "/privacidade", label: "Privacidade" },
{ href: "/privacidade#termos", label: "Termos de uso" },
{ href: "/privacidade#cookies", label: "Cookies" },
],
},
];
const socialLinks = [
{ label: "Twitter/X", href: "#", icon: "𝕏" },
{ label: "LinkedIn", href: "#", icon: "in" },
{ label: "Instagram", href: "#", icon: "IG" },
];
export default function Footer() {
return (
<footer className="border-t border-border bg-muted">
<div className="mx-auto max-w-6xl px-5 sm:px-8 py-16">
<div className="grid grid-cols-2 md:grid-cols-4 gap-10">
{/* Brand column */}
<div className="col-span-2 md:col-span-1">
<Link href="/" className="text-xl font-bold text-foreground">
{BRAND_NAME}
</Link>
<p className="mt-3 text-sm text-muted-foreground leading-relaxed">
Óculos AR para alta performance.
<br />
Métricas no campo de visão.
</p>
{/* Newsletter / waiting list mini CTA */}
<div className="mt-6">
<p className="text-xs font-semibold text-foreground mb-2">
Lista de espera
</p>
<Link
href="/contactos"
className="inline-flex items-center gap-2 text-sm text-accent hover:text-accent-dark font-medium transition-colors"
>
Inscrever-me
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 5l7 7-7 7"
/>
</svg>
</Link>
</div>
</div>
{/* Link columns */}
{footerLinks.map((group) => (
<div key={group.title}>
<h3 className="text-sm font-semibold text-foreground mb-4">
{group.title}
</h3>
<ul className="space-y-2.5">
{group.links.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
{/* Bottom bar */}
<div className="mt-12 pt-8 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-xs text-muted-foreground">
© {new Date().getFullYear()} {BRAND_NAME}. Todos os direitos
reservados.
</p>
<div className="flex gap-4">
{socialLinks.map((s) => (
<a
key={s.label}
href={s.href}
aria-label={s.label}
className="flex h-9 w-9 items-center justify-center rounded-lg text-xs font-bold text-muted-foreground hover:text-foreground hover:bg-background transition-all border border-border"
>
{s.icon}
</a>
))}
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,204 @@
import React from "react";
type HUDVariant = "pace" | "power" | "interval" | "cadence";
interface HUDPreviewSVGProps {
variant?: HUDVariant;
className?: string;
}
export default function HUDPreviewSVG({
variant = "pace",
className = "",
}: HUDPreviewSVGProps) {
return (
<svg
viewBox="0 0 400 200"
className={`w-full max-w-md ${className}`}
role="img"
aria-label={`HUD preview: ${variant}`}
>
{/* Background — translucent dark panel */}
<rect
x="10"
y="10"
width="380"
height="180"
rx="16"
fill="#0a0a0a"
opacity="0.85"
/>
<rect
x="10"
y="10"
width="380"
height="180"
rx="16"
fill="none"
stroke="#38bdf8"
strokeWidth="1"
opacity="0.3"
/>
{variant === "pace" && (
<>
{/* Pace label */}
<text x="40" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
RITMO
</text>
<text x="40" y="85" fill="#fafafa" fontSize="28" fontWeight="bold" fontFamily="monospace">
4:32
</text>
<text x="130" y="85" fill="#71717a" fontSize="14" fontFamily="monospace">
/km
</text>
{/* Target */}
<text x="200" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
ALVO
</text>
<text x="200" y="85" fill="#38bdf8" fontSize="28" fontWeight="bold" fontFamily="monospace">
4:35
</text>
{/* Delta */}
<text x="310" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
DELTA
</text>
<text x="310" y="85" fill="#22c55e" fontSize="24" fontWeight="bold" fontFamily="monospace">
-3s
</text>
{/* Split bar */}
<text x="40" y="130" fill="#71717a" fontSize="11" fontFamily="monospace">
SPLIT 5km
</text>
<rect x="40" y="140" width="320" height="6" rx="3" fill="#27272a" />
<rect x="40" y="140" width="200" height="6" rx="3" fill="#38bdf8" />
{/* Time */}
<text x="40" y="175" fill="#a1a1aa" fontSize="13" fontFamily="monospace">
22:41
</text>
<text x="310" y="175" fill="#a1a1aa" fontSize="13" fontFamily="monospace">
km 5/10
</text>
</>
)}
{variant === "power" && (
<>
<text x="40" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
POTÊNCIA
</text>
<text x="40" y="85" fill="#fafafa" fontSize="28" fontWeight="bold" fontFamily="monospace">
285
</text>
<text x="110" y="85" fill="#71717a" fontSize="14" fontFamily="monospace">
W
</text>
<text x="200" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
ZONA
</text>
<rect x="200" y="65" width="60" height="24" rx="6" fill="#f59e0b" opacity="0.2" />
<text x="212" y="83" fill="#f59e0b" fontSize="14" fontWeight="bold" fontFamily="monospace">
Z4
</text>
<text x="310" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
RITMO
</text>
<text x="310" y="85" fill="#a1a1aa" fontSize="22" fontFamily="monospace">
4:28
</text>
<text x="40" y="130" fill="#71717a" fontSize="11" fontFamily="monospace">
POTÊNCIA MÉDIA
</text>
<text x="40" y="155" fill="#38bdf8" fontSize="20" fontFamily="monospace">
278 W
</text>
<text x="310" y="175" fill="#a1a1aa" fontSize="13" fontFamily="monospace">
34:12
</text>
</>
)}
{variant === "interval" && (
<>
<text x="40" y="50" fill="#f59e0b" fontSize="12" fontWeight="bold" fontFamily="monospace">
INTERVALO 4/8
</text>
<text x="40" y="85" fill="#71717a" fontSize="11" fontFamily="monospace">
TEMPO RESTANTE
</text>
<text x="40" y="115" fill="#fafafa" fontSize="32" fontWeight="bold" fontFamily="monospace">
1:23
</text>
<text x="200" y="85" fill="#71717a" fontSize="11" fontFamily="monospace">
RITMO ATUAL
</text>
<text x="200" y="115" fill="#22c55e" fontSize="28" fontWeight="bold" fontFamily="monospace">
3:48
</text>
<text x="40" y="155" fill="#71717a" fontSize="11" fontFamily="monospace">
RECUPERAÇÃO
</text>
<text x="40" y="175" fill="#a1a1aa" fontSize="14" fontFamily="monospace">
próx: 2:00 trote
</text>
<text x="310" y="175" fill="#a1a1aa" fontSize="13" fontFamily="monospace">
HR 172
</text>
</>
)}
{variant === "cadence" && (
<>
<text x="40" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
CADÊNCIA
</text>
<text x="40" y="85" fill="#fafafa" fontSize="28" fontWeight="bold" fontFamily="monospace">
182
</text>
<text x="110" y="85" fill="#71717a" fontSize="14" fontFamily="monospace">
spm
</text>
{/* Zone indicator */}
<text x="200" y="55" fill="#71717a" fontSize="11" fontFamily="monospace">
ZONA
</text>
<rect x="200" y="65" width="60" height="24" rx="6" fill="#22c55e" opacity="0.2" />
<text x="208" y="83" fill="#22c55e" fontSize="14" fontWeight="bold" fontFamily="monospace">
IDEAL
</text>
<text x="40" y="130" fill="#71717a" fontSize="11" fontFamily="monospace">
GCT
</text>
<text x="40" y="155" fill="#a1a1aa" fontSize="18" fontFamily="monospace">
218 ms
</text>
<text x="200" y="130" fill="#71717a" fontSize="11" fontFamily="monospace">
OSCILAÇÃO
</text>
<text x="200" y="155" fill="#a1a1aa" fontSize="18" fontFamily="monospace">
7.2 cm
</text>
<text x="310" y="175" fill="#a1a1aa" fontSize="13" fontFamily="monospace">
4:32/km
</text>
</>
)}
</svg>
);
}

103
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,103 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { BRAND_NAME } from "@/lib/constants";
import Button from "./Button";
const navLinks = [
{ href: "/produto", label: "Produto" },
{ href: "/performance", label: "Performance" },
{ href: "/treinos-guiados", label: "Treinos" },
{ href: "/treinadores-e-clubes", label: "Treinadores" },
{ href: "/tecnologia", label: "Tecnologia" },
{ href: "/faq", label: "FAQ" },
{ href: "/contactos", label: "Contactos" },
];
export default function Header() {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-lg">
<div className="mx-auto flex max-w-6xl items-center justify-between px-5 sm:px-8 h-16">
{/* Logo */}
<Link
href="/"
className="text-xl font-bold tracking-tight text-foreground hover:text-accent transition-colors"
>
{BRAND_NAME}
</Link>
{/* Desktop nav */}
<nav className="hidden lg:flex items-center gap-1" aria-label="Navegação principal">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted"
>
{link.label}
</Link>
))}
<Button
href="/contactos"
size="sm"
trackEvent="header_pedir_demo"
className="ml-3"
>
Pedir demo
</Button>
</nav>
{/* Mobile toggle */}
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="lg:hidden flex items-center justify-center h-10 w-10 rounded-lg hover:bg-muted transition-colors cursor-pointer"
aria-label={mobileOpen ? "Fechar menu" : "Abrir menu"}
aria-expanded={mobileOpen}
>
{mobileOpen ? (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
{/* Mobile nav */}
{mobileOpen && (
<nav
className="lg:hidden border-t border-border bg-background px-5 py-4 animate-fade-in"
aria-label="Navegação principal (mobile)"
>
<div className="flex flex-col gap-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
className="px-4 py-3 text-base font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
>
{link.label}
</Link>
))}
<div className="pt-3 mt-2 border-t border-border">
<Button
href="/contactos"
trackEvent="mobile_pedir_demo"
className="w-full"
>
Pedir demo
</Button>
</div>
</div>
</nav>
)}
</header>
);
}

View File

@@ -0,0 +1,34 @@
import React from "react";
import Container from "./Container";
interface PageHeroProps {
title: string;
description?: string;
children?: React.ReactNode;
}
export default function PageHero({
title,
description,
children,
}: PageHeroProps) {
return (
<section className="pt-16 pb-12 md:pt-24 md:pb-16">
<Container>
<div className="max-w-3xl">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight animate-fade-in">
{title}
</h1>
{description && (
<p className="mt-5 text-lg md:text-xl text-muted-foreground leading-relaxed animate-fade-in-delay">
{description}
</p>
)}
{children && (
<div className="mt-8 animate-fade-in-delay-2">{children}</div>
)}
</div>
</Container>
</section>
);
}

View File

@@ -0,0 +1,25 @@
import React from "react";
import Container from "./Container";
interface SectionProps {
children: React.ReactNode;
className?: string;
id?: string;
muted?: boolean;
}
export default function Section({
children,
className = "",
id,
muted = false,
}: SectionProps) {
return (
<section
id={id}
className={`py-20 md:py-28 ${muted ? "bg-muted" : ""} ${className}`}
>
<Container>{children}</Container>
</section>
);
}

57
src/components/Tabs.tsx Normal file
View File

@@ -0,0 +1,57 @@
"use client";
import React, { useState } from "react";
interface Tab {
id: string;
label: string;
content: React.ReactNode;
}
interface TabsProps {
tabs: Tab[];
className?: string;
}
export default function Tabs({ tabs, className = "" }: TabsProps) {
const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? "");
return (
<div className={className}>
<div
className="flex gap-1 rounded-xl bg-muted p-1 mb-8"
role="tablist"
aria-label="Separadores de conteúdo"
>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={activeTab === tab.id}
aria-controls={`tabpanel-${tab.id}`}
id={`tab-${tab.id}`}
onClick={() => setActiveTab(tab.id)}
className={`flex-1 rounded-lg px-4 py-2.5 text-sm font-semibold transition-all duration-200 cursor-pointer ${activeTab === tab.id
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab) => (
<div
key={tab.id}
role="tabpanel"
id={`tabpanel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeTab !== tab.id}
className="animate-fade-in"
>
{tab.content}
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React from "react";
export default function TestimonialPlaceholder({
className = "",
}: {
className?: string;
}) {
return (
<div
className={`rounded-2xl border border-border bg-card p-8 text-center ${className}`}
>
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-accent/10">
<svg
className="h-6 w-6 text-accent"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p className="text-lg font-semibold text-foreground mb-2">
Em testes com atletas e treinadores
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
Dados consistentes, alertas mínimos, foco máximo. Resultados de testes
internos com corredores de alta competição.
</p>
</div>
);
}

9
src/lib/analytics.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Analytics utility — vendor-agnostic event tracking.
* Currently logs to console. Replace with your analytics provider.
*/
export function track(eventName: string, properties?: Record<string, unknown>) {
if (typeof window !== "undefined") {
console.log(`[Analytics] ${eventName}`, properties ?? {});
}
}

244
src/lib/blog-data.ts Normal file
View File

@@ -0,0 +1,244 @@
export interface BlogPost {
slug: string;
title: string;
description: string;
date: string;
tags: string[];
readingTime: string;
body: string;
}
export const blogPosts: BlogPost[] = [
{
slug: "ritmo-vs-potencia-no-treino",
title: "Ritmo vs. potência no treino de corrida",
description:
"Qual métrica deve guiar o teu treino? Entende as diferenças entre pacing por ritmo e por potência, e como tirar partido de ambas.",
date: "2026-01-15",
tags: ["treino", "pacing", "potência"],
readingTime: "5 min",
body: `## Ritmo ou potência: qual seguir?
O ritmo (pace) é a métrica mais intuitiva — minutos por quilómetro. É fácil de entender e comparar. No entanto, depende enormemente de fatores externos: vento, inclinação, temperatura e até tipo de superfície.
A potência (watts), por outro lado, mede o esforço mecânico real. É independente das condições externas e responde instantaneamente à mudança de esforço.
### Quando usar ritmo
- **Provas em circuito plano** — o pace é previsível e fácil de controlar.
- **Comparação entre treinos** — quando as condições são semelhantes.
- **Objetivos de tempo** — a meta final é expressa em ritmo.
### Quando usar potência
- **Treinos em terreno variável** — subidas e descidas distorcem o pace.
- **Intervalos curtos** — a potência responde em 12 segundos; o GPS pode demorar 510.
- **Gestão de esforço em trail** — manter watts constantes otimiza a energia.
### A combinação ideal
Atletas de elite utilizam ambas as métricas. O ritmo define a meta; a potência gere o esforço em tempo real. Com um HUD, estas métricas estão sempre no campo de visão, sem necessidade de olhar para o pulso.
> Com feedback visual em tempo real, decisões de pacing tornam-se instintivas.`,
},
{
slug: "splits-e-estrategia-de-prova",
title: "Splits e estratégia de prova: como planear cada quilómetro",
description:
"Aprende a definir uma estratégia de splits para maximizar a performance em provas de 5K a maratona.",
date: "2026-01-22",
tags: ["splits", "corrida", "estratégia"],
readingTime: "6 min",
body: `## A importância de uma estratégia de splits
Correr uma prova sem plano de splits é como navegar sem mapa. Muitos atletas saem demasiado rápido nos primeiros quilómetros e pagam o preço na segunda metade.
### Tipos de estratégia
**Even splits** — manter o mesmo ritmo do início ao fim. É a abordagem mais eficiente fisiologicamente, recomendada para a maioria dos corredores em provas de 10K a maratona.
**Negative splits** — a segunda metade é mais rápida que a primeira. Exige disciplina, mas maximiza o desempenho em provas longas. Requer uma partida controlada.
**Positive splits** — a primeira metade é mais rápida. Comum em sprints e provas até 1500m, onde a fadiga é inevitável e o objetivo é criar vantagem inicial.
### Como definir os teus splits
1. **Define o tempo-alvo final** — por exemplo, 3:30 para maratona (4:59/km).
2. **Ajusta para o perfil do percurso** — subidas exigem mais tempo; descidas permitem recuperar.
3. **Calcula margens de segurança** — permite 23 segundos de folga por km na primeira metade.
4. **Pratica em treino** — treinos de ritmo específico (tempo runs) são essenciais.
### Feedback visual durante a prova
Com um display heads-up, podes ver o delta entre o ritmo atual e o planeado — em tempo real, sem desviar os olhos da estrada. Isso elimina o erro de perceção e reduz o stress cognitivo.
> A melhor estratégia de prova é a que se executa sem pensar. O feedback visual torna isso possível.`,
},
{
slug: "cadencia-e-eficiencia",
title: "Cadência e eficiência na corrida: o que os números dizem",
description:
"Descobre como a cadência influencia a eficiência mecânica e como otimizar os teus passos por minuto.",
date: "2026-01-29",
tags: ["cadência", "biomecânica", "eficiência"],
readingTime: "5 min",
body: `## O que é a cadência?
A cadência (ou step rate) é o número de passos por minuto (spm). É uma das métricas biomecânicas mais simples de medir e mais impactantes de otimizar.
### O mito dos 180 spm
A referência de 180 spm vem de observações de Jack Daniels nos Jogos Olímpicos de 1984. No entanto, a cadência ideal varia conforme:
- **Velocidade** — velocidades mais altas naturalmente exigem cadências mais altas.
- **Altura e comprimento de perna** — corredores mais altos tendem a ter cadências mais baixas.
- **Tipo de terreno** — trail exige adaptação constante.
### Porque é que a cadência importa
Uma cadência mais alta (mantendo a velocidade) geralmente implica:
- Menor tempo de contacto com o solo
- Menor impacto por passo
- Maior eficiência energética
- Redução do risco de certas lesões
### Como monitorizar em tempo real
O display HUD pode mostrar a cadência atual com indicadores visuais de zona (verde/amarelo/vermelho), permitindo ajustes imediatos sem interromper o fluxo natural da corrida.
> Pequenas mudanças de cadência — 3 a 5% — podem gerar melhorias significativas de eficiência ao longo de milhares de passos.`,
},
{
slug: "como-ler-sinais-de-fadiga",
title: "Como ler sinais de fadiga durante o treino e a prova",
description:
"Aprende a identificar sinais precoces de fadiga através de métricas objetivas e ajusta o teu esforço em tempo real.",
date: "2026-02-05",
tags: ["fadiga", "monitorização", "treino"],
readingTime: "6 min",
body: `## Fadiga: o inimigo invisível
A fadiga acumula-se gradualmente e, frequentemente, o atleta só a percebe quando já é tarde para reagir. Métricas objetivas podem ajudar a identificar sinais precoces.
### Indicadores objetivos de fadiga
**Cardiac drift** — Quando a frequência cardíaca sobe progressivamente para o mesmo ritmo. Se o teu pace é constante mas o HR sobe 1015 bpm ao longo do treino, o corpo está a compensar a fadiga.
**Queda de cadência** — Uma redução progressiva de cadência (25 spm) ao longo de uma sessão pode indicar fadiga neuromuscular.
**Aumento do tempo de contacto com o solo** — O solo "agarra" mais quando os músculos estão fatigados. É uma métrica subtil mas relevante.
**Oscilação vertical** — Mais bounce geralmente indica menos eficiência — sinal de que os músculos estabilizadores estão a perder eficácia.
### Como agir perante sinais de fadiga
1. **Em treino** — Reduzir intensidade ou encurtar a sessão. Treinar fatigado pode ser útil, mas exige consciência.
2. **Em prova** — Ajustar o plano de splits. Melhor um negative split modesto do que um colapso.
3. **Na recuperação** — Monitorizar a resposta cardíaca e muscular nas 2448h seguintes.
### O papel do HUD
Com as métricas certas no campo de visão, o atleta pode detetar padrões de fadiga em tempo real e tomar decisões informadas — em vez de depender apenas da perceção subjetiva.
> Correr inteligente é saber quando abrandar. Os dados ajudam a tomar essa decisão antes que o corpo o faça por ti.`,
},
{
slug: "intervalos-na-pista-exemplos",
title: "Intervalos na pista: exemplos práticos para cada objetivo",
description:
"Sessões de intervalos na pista para desenvolver VO2máx, limiar e velocidade — com tempos de referência e recuperação.",
date: "2026-02-10",
tags: ["intervalos", "pista", "treino"],
readingTime: "7 min",
body: `## Porque treinar intervalos na pista
A pista é o laboratório do corredor. Distâncias exatas, superfície uniforme e controlo total sobre as variáveis. Os intervalos são a forma mais eficaz de desenvolver capacidades específicas.
### Sessões para VO2máx
O VO2máx é a capacidade máxima de consumo de oxigénio. Treina-se com intervalos de 35 minutos a intensidade alta.
**Exemplo 1: 5×1000m**
- Ritmo: pace de 3K5K
- Recuperação: 23 min trote leve
- Total de trabalho: 5000m
**Exemplo 2: 6×800m**
- Ritmo: pace de 3K
- Recuperação: 2 min
- Total de trabalho: 4800m
### Sessões para limiar anaeróbio
O limiar é a intensidade máxima sustentável. Treina-se com intervalos longos a ritmo controlado.
**Exemplo: 3×2000m**
- Ritmo: pace de 10K a meia-maratona
- Recuperação: 90s2 min
- Total de trabalho: 6000m
### Sessões de velocidade
Para melhorar a economia de corrida e recrutar fibras rápidas.
**Exemplo: 10×200m**
- Ritmo: pace de 800m1500m
- Recuperação: 200m a caminhar
- Total de trabalho: 2000m
### Monitorização com HUD
Na pista, o HUD pode mostrar split parcial em tempo real, delta face ao ritmo-alvo e tempo de recuperação restante — sem que o atleta precise de parar para consultar o relógio.
> A pista é onde se constrói velocidade. O HUD é onde se garante precisão.`,
},
{
slug: "prevencao-de-lesoes-e-assimetria",
title: "Prevenção de lesões e assimetria: o que a biomecânica revela",
description:
"Como a análise de assimetrias biomecânicas pode ajudar a identificar riscos — sem substituir avaliação clínica.",
date: "2026-02-11",
tags: ["lesões", "biomecânica", "prevenção"],
readingTime: "6 min",
body: `## Assimetria e risco — com cautela
> **Nota:** Esta informação é educativa e não substitui avaliação médica ou fisioterapêutica. Qualquer decisão sobre prevenção ou tratamento de lesões deve envolver profissionais de saúde qualificados.
### O que é a assimetria biomecânica?
Nenhum corredor é perfeitamente simétrico. Todos temos ligeiras diferenças entre perna esquerda e direita — em tempo de contacto, cadência unilateral e produção de força.
A questão não é eliminar a assimetria, mas sim monitorizar mudanças significativas ao longo do tempo.
### Indicadores que podem ser relevantes
**Diferença de tempo de contacto** — Se uma perna passa consistentemente mais tempo no solo (>5% de diferença), pode haver compensação por fadiga, dor ou desequilíbrio muscular.
**Assimetria de potência** — Diferenças na produção de energia entre pernas. Variações <3% são geralmente normais.
**Padrões de cadência** — Se a cadência de um lado cai consistentemente durante treinos longos, pode indicar fadiga localizada.
### O que fazer com estes dados
1. **Não auto-diagnosticar** — assimetria não significa lesão.
2. **Registar tendências** — valores pontuais são menos úteis que tendências ao longo de semanas.
3. **Partilhar com o treinador ou fisioterapeuta** — os dados são mais úteis quando interpretados por um profissional.
4. **Usar como sinal de alerta precoce** — se a assimetria aumenta subitamente, pode merecer atenção.
### Papel da monitorização contínua
Dispositivos que recolhem dados biomecânicos em tempo real podem detetar alterações subtis que o atleta não percebe conscientemente. Não se trata de diagnóstico — trata-se de informação.
> Dados biomecânicos não substituem um profissional de saúde. São uma ferramenta de alerta, não de diagnóstico.`,
},
];
export function getPostBySlug(slug: string): BlogPost | undefined {
return blogPosts.find((p) => p.slug === slug);
}
export function getAllPosts(): BlogPost[] {
return blogPosts.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
}

6
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,6 @@
/** Brand and product name placeholders — easy to find-and-replace */
export const BRAND_NAME = "BRAND_NAME";
export const PRODUCT_NAME = "PRODUCT_NAME";
export const SITE_URL = "https://runvisionpro.pt";
export const SITE_DESCRIPTION =
"Óculos AR de alta performance para atletas e treinadores de corrida. Métricas em tempo real no campo de visão.";

47
src/lib/submissions.ts Normal file
View File

@@ -0,0 +1,47 @@
/**
* In-memory submission storage.
* TODO: Replace with Supabase, Resend, or any database.
*/
export interface DemoRequest {
nome: string;
email: string;
organizacao?: string;
papel: string;
numAtletas?: string;
mensagem?: string;
createdAt: string;
}
export interface WaitingListEntry {
nome: string;
email: string;
perfil: string;
distancia?: string;
objetivo?: string;
createdAt: string;
}
// In-memory stores — data is lost on server restart
const demoRequests: DemoRequest[] = [];
const waitingList: WaitingListEntry[] = [];
export function addDemoRequest(data: DemoRequest) {
demoRequests.push(data);
console.log("[Submission] Demo request:", data);
return data;
}
export function addWaitingListEntry(data: WaitingListEntry) {
waitingList.push(data);
console.log("[Submission] Waiting list:", data);
return data;
}
export function getDemoRequests() {
return demoRequests;
}
export function getWaitingListEntries() {
return waitingList;
}