first commit
This commit is contained in:
122
src/app/LeadCaptureForm.tsx
Normal file
122
src/app/LeadCaptureForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
src/app/api/demo-request/route.ts
Normal file
41
src/app/api/demo-request/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/app/api/waiting-list/route.ts
Normal file
40
src/app/api/waiting-list/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
18
src/app/blog/[slug]/BlogPostTracker.tsx
Normal file
18
src/app/blog/[slug]/BlogPostTracker.tsx
Normal 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;
|
||||
}
|
||||
149
src/app/blog/[slug]/page.tsx
Normal file
149
src/app/blog/[slug]/page.tsx
Normal 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
79
src/app/blog/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
123
src/app/casos-de-uso/CasosDeUsoTabs.tsx
Normal file
123
src/app/casos-de-uso/CasosDeUsoTabs.tsx
Normal 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: "200m–400m com recuperação programada." },
|
||||
{ title: "Intervalos longos", desc: "800m–2000m 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: "5K–10K", 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: "25–80 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} />;
|
||||
}
|
||||
33
src/app/casos-de-uso/page.tsx
Normal file
33
src/app/casos-de-uso/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
102
src/app/contactos/DemoRequestForm.tsx
Normal file
102
src/app/contactos/DemoRequestForm.tsx
Normal 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">Nº 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>
|
||||
);
|
||||
}
|
||||
93
src/app/contactos/WaitingListForm.tsx
Normal file
93
src/app/contactos/WaitingListForm.tsx
Normal 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 — 1–2 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. 1–2 atualizações por mês.</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
52
src/app/contactos/page.tsx
Normal file
52
src/app/contactos/page.tsx
Normal 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. Sê 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
84
src/app/faq/page.tsx
Normal 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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,88 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-accent: var(--accent);
|
||||
--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) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--background: #09090b;
|
||||
--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 {
|
||||
background: var(--background);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
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({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
metadataBase: new URL(SITE_URL),
|
||||
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({
|
||||
@@ -23,11 +43,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<html lang="pt-PT">
|
||||
<body className={`${inter.variable} font-sans antialiased`}>
|
||||
<Header />
|
||||
<main className="min-h-screen">{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
298
src/app/page.tsx
298
src/app/page.tsx
@@ -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 (
|
||||
<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">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<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.
|
||||
<>
|
||||
{/* ─── Hero ─── */}
|
||||
<section className="pt-20 pb-16 md:pt-32 md:pb-24">
|
||||
<Container>
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-[1.1] animate-fade-in">
|
||||
Ritmo, potência e feedback em tempo real
|
||||
<span className="text-accent"> — sem tirares os olhos do treino.</span>
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
<p className="mt-6 text-lg md:text-xl text-muted-foreground leading-relaxed animate-fade-in-delay max-w-xl">
|
||||
Óculos AR para atletas e treinadores focados em alta competição.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col sm:flex-row gap-4 animate-fade-in-delay-2">
|
||||
<Button href="/contactos" trackEvent="hero_pedir_demo" size="lg">
|
||||
Pedir demo
|
||||
</Button>
|
||||
<Button href="#video" variant="secondary" size="lg" trackEvent="hero_ver_como_funciona">
|
||||
Ver como funciona
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center animate-fade-in-delay">
|
||||
<div className="w-full max-w-lg rounded-2xl border border-border bg-card p-6 shadow-xl">
|
||||
<HUDPreviewSVG variant="pace" />
|
||||
</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">
|
||||
Vê 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>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
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]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<div className="relative mx-auto max-w-4xl aspect-video rounded-2xl bg-muted border border-border overflow-hidden flex items-center justify-center">
|
||||
{/* Poster frame */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-accent/5 to-transparent" />
|
||||
<button
|
||||
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"
|
||||
aria-label="Reproduzir vídeo demonstrativo"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<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]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<svg className="h-8 w-8 ml-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
<p className="absolute bottom-6 text-sm text-muted-foreground">
|
||||
Demonstração — vídeo em breve
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ─── Before vs After ─── */}
|
||||
<Section muted>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12">
|
||||
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
|
||||
</a>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</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>
|
||||
</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">
|
||||
Sê dos primeiros a experimentar
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
Sem spam. 1–2 atualizações por mês.
|
||||
</p>
|
||||
<LeadCaptureForm />
|
||||
</div>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
138
src/app/performance/page.tsx
Normal file
138
src/app/performance/page.tsx
Normal 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 5–10 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
81
src/app/privacidade/page.tsx
Normal file
81
src/app/privacidade/page.tsx
Normal 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
93
src/app/produto/page.tsx
Normal 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
15
src/app/robots.ts
Normal 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
37
src/app/sitemap.ts
Normal 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
94
src/app/sobre/page.tsx
Normal 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 1–2 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
131
src/app/tecnologia/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
117
src/app/treinadores-e-clubes/page.tsx
Normal file
117
src/app/treinadores-e-clubes/page.tsx
Normal 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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
133
src/app/treinos-guiados/page.tsx
Normal file
133
src/app/treinos-guiados/page.tsx
Normal 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
62
src/components/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
src/components/CTASection.tsx
Normal file
42
src/components/CTASection.tsx
Normal 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
24
src/components/Card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
src/components/Container.tsx
Normal file
19
src/components/Container.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
69
src/components/FAQAccordion.tsx
Normal file
69
src/components/FAQAccordion.tsx
Normal 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
128
src/components/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
204
src/components/HUDPreviewSVG.tsx
Normal file
204
src/components/HUDPreviewSVG.tsx
Normal 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
103
src/components/Header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
src/components/PageHero.tsx
Normal file
34
src/components/PageHero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
src/components/Section.tsx
Normal file
25
src/components/Section.tsx
Normal 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
57
src/components/Tabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
36
src/components/TestimonialPlaceholder.tsx
Normal file
36
src/components/TestimonialPlaceholder.tsx
Normal 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
9
src/lib/analytics.ts
Normal 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
244
src/lib/blog-data.ts
Normal 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 1–2 segundos; o GPS pode demorar 5–10.
|
||||
- **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 2–3 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 10–15 bpm ao longo do treino, o corpo está a compensar a fadiga.
|
||||
|
||||
**Queda de cadência** — Uma redução progressiva de cadência (2–5 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 24–48h 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 3–5 minutos a intensidade alta.
|
||||
|
||||
**Exemplo 1: 5×1000m**
|
||||
- Ritmo: pace de 3K–5K
|
||||
- Recuperação: 2–3 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: 90s–2 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 800m–1500m
|
||||
- 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
6
src/lib/constants.ts
Normal 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
47
src/lib/submissions.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user