Initial commit
This commit is contained in:
@@ -0,0 +1,504 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type SpeechRecognitionInstance = {
|
||||
lang: string;
|
||||
interimResults: boolean;
|
||||
continuous: boolean;
|
||||
onresult: ((event: { results: Array<{ 0?: { transcript?: string } }> }) => void) | null;
|
||||
onerror: ((event: unknown) => void) | null;
|
||||
onend: (() => void) | null;
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
};
|
||||
|
||||
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
|
||||
|
||||
type ChatMessage = {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type ApiResponse = {
|
||||
response: string;
|
||||
shouldCaptureEmail: boolean;
|
||||
suggestedActions: string[];
|
||||
};
|
||||
|
||||
type SiteMenteVoiceWidgetProps = {
|
||||
initialLang?: "es" | "en";
|
||||
};
|
||||
|
||||
const quickActions = {
|
||||
es: [
|
||||
{ label: "¿Cuánto cuesta?", icon: "💰" },
|
||||
{ label: "Ver casos de éxito", icon: "🎯" },
|
||||
{ label: "¿Cómo funciona?", icon: "⚙️" },
|
||||
],
|
||||
en: [
|
||||
{ label: "Pricing?", icon: "💰" },
|
||||
{ label: "Success stories", icon: "🎯" },
|
||||
{ label: "How it works?", icon: "⚙️" },
|
||||
],
|
||||
} as const;
|
||||
|
||||
const initialGreeting = {
|
||||
es: "Hola, soy el cerebro de SiteMente. ¿En qué te puedo ayudar hoy?",
|
||||
en: "Hi, I'm the SiteMente brain. How can I help you today?",
|
||||
} as const;
|
||||
|
||||
export default function SiteMenteVoiceWidget({
|
||||
initialLang = "es",
|
||||
}: SiteMenteVoiceWidgetProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [lang, setLang] = useState<"es" | "en">(initialLang);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
role: "assistant",
|
||||
content: initialGreeting[initialLang],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
]);
|
||||
const [input, setInput] = useState("");
|
||||
const [voiceMode, setVoiceMode] = useState(true);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [transcript, setTranscript] = useState("");
|
||||
const [speechSupported, setSpeechSupported] = useState(true);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
|
||||
const isRecordingRef = useRef(false);
|
||||
const transcriptRef = useRef("");
|
||||
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const localeLabel = useMemo(
|
||||
() => (lang === "es" ? "ES" : "EN"),
|
||||
[lang]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLang(initialLang);
|
||||
}, [initialLang]);
|
||||
|
||||
useEffect(() => {
|
||||
const seen = window.localStorage.getItem("sitemente:voice-tooltip");
|
||||
if (!seen) {
|
||||
setShowTooltip(true);
|
||||
window.localStorage.setItem("sitemente:voice-tooltip", "1");
|
||||
const timeout = window.setTimeout(() => setShowTooltip(false), 4000);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
isRecordingRef.current = isRecording;
|
||||
}, [isRecording]);
|
||||
|
||||
useEffect(() => {
|
||||
transcriptRef.current = transcript;
|
||||
}, [transcript]);
|
||||
|
||||
useEffect(() => {
|
||||
const SpeechRecognitionImpl =
|
||||
typeof window !== "undefined"
|
||||
? ((window as typeof window & {
|
||||
webkitSpeechRecognition?: SpeechRecognitionConstructor;
|
||||
}).SpeechRecognition ||
|
||||
(window as typeof window & {
|
||||
webkitSpeechRecognition?: SpeechRecognitionConstructor;
|
||||
}).webkitSpeechRecognition)
|
||||
: undefined;
|
||||
|
||||
if (!SpeechRecognitionImpl) {
|
||||
setSpeechSupported(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const recognition = new SpeechRecognitionImpl();
|
||||
recognition.lang = lang === "es" ? "es-ES" : "en-US";
|
||||
recognition.interimResults = true;
|
||||
recognition.continuous = false;
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
const result = Array.from(event.results)
|
||||
.map((res) => res[0]?.transcript ?? "")
|
||||
.join(" ");
|
||||
setTranscript(result.trim());
|
||||
};
|
||||
|
||||
recognition.onerror = () => {
|
||||
setIsRecording(false);
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
if (isRecordingRef.current) {
|
||||
setIsRecording(false);
|
||||
const finalTranscript = transcriptRef.current.trim();
|
||||
if (finalTranscript) {
|
||||
handleSend(finalTranscript);
|
||||
}
|
||||
setTranscript("");
|
||||
}
|
||||
};
|
||||
|
||||
recognitionRef.current = recognition;
|
||||
}, [lang]);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSpeaking) return;
|
||||
return () => {
|
||||
window.speechSynthesis?.cancel();
|
||||
};
|
||||
}, [isSpeaking]);
|
||||
|
||||
useEffect(() => {
|
||||
setMessages((prev) => {
|
||||
if (prev.length === 0) return prev;
|
||||
const updated = [...prev];
|
||||
if (updated[0].role === "assistant") {
|
||||
updated[0] = {
|
||||
...updated[0],
|
||||
content: initialGreeting[lang],
|
||||
};
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, [lang]);
|
||||
|
||||
const startRecording = () => {
|
||||
if (!speechSupported || !recognitionRef.current) return;
|
||||
setTranscript("");
|
||||
setIsRecording(true);
|
||||
recognitionRef.current.start();
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (!recognitionRef.current) return;
|
||||
recognitionRef.current.stop();
|
||||
setIsRecording(false);
|
||||
};
|
||||
|
||||
const speak = (text: string) => {
|
||||
if (!("speechSynthesis" in window)) return;
|
||||
window.speechSynthesis.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = lang === "es" ? "es-ES" : "en-US";
|
||||
utterance.onstart = () => setIsSpeaking(true);
|
||||
utterance.onend = () => setIsSpeaking(false);
|
||||
utterance.onerror = () => setIsSpeaking(false);
|
||||
window.speechSynthesis.speak(utterance);
|
||||
};
|
||||
|
||||
const handleSend = async (text: string) => {
|
||||
if (!text.trim() || isLoading) return;
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: text,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/chat/agent", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
message: text,
|
||||
locale: lang,
|
||||
history: messages.slice(-6),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch response.");
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ApiResponse;
|
||||
const assistantMessage: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: data.response,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
|
||||
if (voiceMode) {
|
||||
speak(data.response);
|
||||
}
|
||||
} catch (error) {
|
||||
const fallbackMessage: ChatMessage = {
|
||||
role: "assistant",
|
||||
content:
|
||||
lang === "es"
|
||||
? "Hubo un problema al responder. ¿Quieres intentarlo de nuevo?"
|
||||
: "There was a problem responding. Want to try again?",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setMessages((prev) => [...prev, fallbackMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const voiceIndicator = isRecording
|
||||
? "🎤"
|
||||
: isSpeaking
|
||||
? "🔊"
|
||||
: "🎤";
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOpen && (
|
||||
<div className="fixed bottom-6 right-6 z-[9999] flex flex-col items-end gap-2">
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="relative flex h-[68px] w-[68px] items-center justify-center rounded-full bg-gradient-to-br from-[#8B5CF6] to-[#EC4899] text-white shadow-lg transition hover:scale-110 hover:shadow-[0_12px_30px_rgba(236,72,153,0.45)]"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="h-3 w-1 rounded-full bg-white/80 animate-pulse" />
|
||||
<span className="h-5 w-1 rounded-full bg-white/90 animate-pulse" />
|
||||
<span className="h-4 w-1 rounded-full bg-white/80 animate-pulse" />
|
||||
</div>
|
||||
</button>
|
||||
{showTooltip && (
|
||||
<div className="absolute right-[76px] top-1/2 -translate-y-1/2 rounded-full bg-white px-3 py-1 text-xs font-semibold text-brand-purple-dark shadow-md">
|
||||
{lang === "es" ? "Prueba la voz" : "Try voice"}
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -top-6 right-2 rounded-full bg-white/20 px-2 py-1 text-[10px] font-semibold text-white backdrop-blur">
|
||||
▶ Demo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-[9999] flex items-end justify-end p-4 sm:p-6">
|
||||
<div className="absolute inset-0 bg-black/50" onClick={() => setIsOpen(false)} />
|
||||
<div className="relative z-10 flex h-full w-full max-w-[440px] flex-col overflow-hidden rounded-3xl border border-white/15 bg-[#4f3a78] shadow-[0_30px_80px_rgba(0,0,0,0.45)] sm:h-[700px]">
|
||||
<div className="flex items-center justify-between bg-gradient-to-r from-[#6d4cc2] to-[#ff66b5] px-5 py-4 text-white">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-lg font-semibold">
|
||||
<span>SiteMente IA</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
|
||||
<span className="h-3 w-1 rounded-full bg-white/90 animate-pulse" />
|
||||
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/80">
|
||||
{lang === "es" ? "El cerebro de tu web" : "Your website brain"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-lg">
|
||||
<span>{voiceIndicator}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLang((prev) => (prev === "es" ? "en" : "es"))}
|
||||
className="rounded-full border border-white/30 px-2 py-1 text-xs font-semibold"
|
||||
>
|
||||
{localeLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||
{messages.length === 1 && (
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{quickActions[lang].map((action) => (
|
||||
<button
|
||||
key={action.label}
|
||||
type="button"
|
||||
onClick={() => handleSend(action.label)}
|
||||
className="rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs text-white/90 transition hover:bg-white/20"
|
||||
>
|
||||
{action.icon} {action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={`${message.timestamp}-${message.role}`}
|
||||
className={`group flex ${
|
||||
message.role === "user" ? "justify-end" : "justify-start"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm shadow-md ${
|
||||
message.role === "user"
|
||||
? "bg-[#c4a1ff] text-[#3b1c66]"
|
||||
: "bg-[#6a4bb0] text-white"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{message.role === "assistant" && (
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-white/20 text-xs font-semibold">
|
||||
SM
|
||||
</span>
|
||||
)}
|
||||
<p>{message.content}</p>
|
||||
</div>
|
||||
<span className="mt-2 block text-[10px] text-white/60 opacity-0 transition group-hover:opacity-100">
|
||||
{new Date(message.timestamp).toLocaleTimeString(
|
||||
lang === "es" ? "es-ES" : "en-US",
|
||||
{ hour: "2-digit", minute: "2-digit" }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 text-white/70">
|
||||
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/70" />
|
||||
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/60 delay-150" />
|
||||
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/50 delay-300" />
|
||||
</div>
|
||||
)}
|
||||
{isSpeaking && (
|
||||
<div className="flex items-center gap-2 text-xs text-white/70">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
|
||||
<span className="h-3 w-1 rounded-full bg-white/90 animate-pulse" />
|
||||
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
|
||||
</span>
|
||||
{lang === "es" ? "Hablando..." : "Speaking..."}
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 bg-[#4a3572] px-5 py-4">
|
||||
{voiceMode ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{transcript && (
|
||||
<p className="w-full rounded-xl bg-white/10 px-4 py-2 text-sm text-white/90">
|
||||
{transcript}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
return;
|
||||
}
|
||||
if (isSpeaking) {
|
||||
window.speechSynthesis?.cancel();
|
||||
setIsSpeaking(false);
|
||||
return;
|
||||
}
|
||||
startRecording();
|
||||
}}
|
||||
className={`flex h-14 w-14 items-center justify-center rounded-full text-white transition ${
|
||||
isRecording
|
||||
? "bg-red-500 animate-pulse"
|
||||
: isSpeaking
|
||||
? "bg-blue-500 animate-pulse"
|
||||
: "bg-white/20 hover:bg-white/30"
|
||||
}`}
|
||||
disabled={!speechSupported}
|
||||
>
|
||||
{isRecording ? "🔴" : isSpeaking ? "🔊" : "🎤"}
|
||||
</button>
|
||||
<p className="text-xs text-white/80">
|
||||
{isRecording
|
||||
? lang === "es"
|
||||
? "Escuchando..."
|
||||
: "Listening..."
|
||||
: isSpeaking
|
||||
? lang === "es"
|
||||
? "Hablando..."
|
||||
: "Speaking..."
|
||||
: lang === "es"
|
||||
? "Toca para hablar"
|
||||
: "Tap to talk"}
|
||||
</p>
|
||||
{isSpeaking && (
|
||||
<div className="h-1 w-full overflow-hidden rounded-full bg-white/10">
|
||||
<div className="h-full w-1/2 animate-pulse rounded-full bg-brand-pink/80" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full items-center justify-between text-xs text-white/70">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVoiceMode(false)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
⌨️ {lang === "es" ? "Texto" : "Text"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.speechSynthesis?.cancel();
|
||||
setIsSpeaking(false);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
⏸ {lang === "es" ? "Pausar" : "Pause"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
placeholder={
|
||||
lang === "es"
|
||||
? "Escribe tu mensaje..."
|
||||
: "Type your message..."
|
||||
}
|
||||
className="flex-1 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-sm text-white placeholder:text-white/50 focus:border-white/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSend(input)}
|
||||
className="rounded-full bg-brand-pink px-4 py-2 text-sm font-semibold text-white"
|
||||
>
|
||||
➤
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVoiceMode(true)}
|
||||
className="rounded-full border border-white/20 px-3 py-2 text-white/80"
|
||||
>
|
||||
🎤
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!speechSupported && (
|
||||
<p className="mt-2 text-center text-xs text-white/60">
|
||||
{lang === "es"
|
||||
? "Tu navegador no soporta voz. Usa el modo texto."
|
||||
: "Your browser doesn't support voice. Use text mode."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Image from "next/image";
|
||||
|
||||
type HeroBannerProps = {
|
||||
priority?: boolean;
|
||||
};
|
||||
|
||||
export default function HeroBanner({ priority = true }: HeroBannerProps) {
|
||||
return (
|
||||
<section className="hero-hover w-full">
|
||||
<div className="relative h-[320px] w-full overflow-hidden bg-[#cfd7ea] sm:h-[420px] lg:h-[560px]">
|
||||
<Image
|
||||
src="/SiteMente-Banner.jpg"
|
||||
alt="SiteMente hero banner with AI voice visual"
|
||||
fill
|
||||
sizes="100vw"
|
||||
priority={priority}
|
||||
className="object-contain"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-white/0 via-white/0 to-white/10" />
|
||||
<div className="hero-voice-ring hero-voice-ring--one" />
|
||||
<div className="hero-voice-ring hero-voice-ring--two" />
|
||||
<div className="hero-voice-ring hero-voice-ring--three" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import Image from "next/image";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type ImageSlide = {
|
||||
type: "image";
|
||||
id: string;
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
type StatItem = {
|
||||
headline: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type StatsSlide = {
|
||||
type: "stats";
|
||||
id: string;
|
||||
title: string;
|
||||
items: StatItem[];
|
||||
};
|
||||
|
||||
export type HeroSlide = ImageSlide | StatsSlide;
|
||||
|
||||
type HeroSliderProps = {
|
||||
slides: HeroSlide[];
|
||||
autoAdvanceMs?: number;
|
||||
};
|
||||
|
||||
export default function HeroSlider({
|
||||
slides,
|
||||
autoAdvanceMs = 7000,
|
||||
}: HeroSliderProps) {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const touchStartX = useRef<number | null>(null);
|
||||
const touchDelta = useRef(0);
|
||||
|
||||
const maxIndex = slides.length - 1;
|
||||
|
||||
const goTo = (index: number) => {
|
||||
if (index < 0) return setActiveIndex(maxIndex);
|
||||
if (index > maxIndex) return setActiveIndex(0);
|
||||
return setActiveIndex(index);
|
||||
};
|
||||
|
||||
const next = () => goTo(activeIndex + 1);
|
||||
const prev = () => goTo(activeIndex - 1);
|
||||
|
||||
const currentSlide = useMemo(() => slides[activeIndex], [slides, activeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaused || slides.length <= 1) return undefined;
|
||||
const timer = window.setInterval(() => {
|
||||
setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
|
||||
}, autoAdvanceMs);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [autoAdvanceMs, isPaused, slides.length]);
|
||||
|
||||
const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
|
||||
touchStartX.current = event.touches[0]?.clientX ?? null;
|
||||
touchDelta.current = 0;
|
||||
};
|
||||
|
||||
const handleTouchMove = (event: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (touchStartX.current === null) return;
|
||||
touchDelta.current =
|
||||
(event.touches[0]?.clientX ?? touchStartX.current) - touchStartX.current;
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
const delta = touchDelta.current;
|
||||
touchStartX.current = null;
|
||||
touchDelta.current = 0;
|
||||
if (Math.abs(delta) < 40) return;
|
||||
if (delta > 0) prev();
|
||||
if (delta < 0) next();
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className="w-full"
|
||||
onMouseEnter={() => setIsPaused(true)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<div className="relative h-[320px] w-full overflow-hidden sm:h-[420px] lg:h-[560px]">
|
||||
{currentSlide.type === "image" ? (
|
||||
<Image
|
||||
src={currentSlide.src}
|
||||
alt={currentSlide.alt}
|
||||
fill
|
||||
sizes="100vw"
|
||||
priority
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-brand-purple-dark px-6">
|
||||
<div className="mx-auto w-full max-w-5xl">
|
||||
<p className="text-center text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
|
||||
{currentSlide.title}
|
||||
</p>
|
||||
<div className="mt-8 grid gap-6 text-center md:grid-cols-3">
|
||||
{currentSlide.items.map((item) => (
|
||||
<div
|
||||
key={item.headline}
|
||||
className="rounded-2xl border border-white/15 bg-white/10 p-6 backdrop-blur"
|
||||
>
|
||||
<p className="text-3xl font-bold text-brand-pink sm:text-4xl">
|
||||
{item.headline}
|
||||
</p>
|
||||
<p className="mt-3 text-sm text-white/85">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-4 flex items-center justify-center gap-2">
|
||||
{slides.map((slide, index) => (
|
||||
<button
|
||||
key={slide.id}
|
||||
className={`pointer-events-auto h-2.5 w-2.5 rounded-full transition ${
|
||||
index === activeIndex
|
||||
? "bg-brand-pink"
|
||||
: "bg-white/50 hover:bg-white"
|
||||
}`}
|
||||
onClick={() => goTo(index)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{slides.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="absolute left-4 top-1/2 hidden -translate-y-1/2 rounded-full border border-white/30 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:block"
|
||||
onClick={prev}
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-4 top-1/2 hidden -translate-y-1/2 rounded-full border border-white/30 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:block"
|
||||
onClick={next}
|
||||
aria-label="Next slide"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 rounded-full border border-white/40 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:hidden"
|
||||
onClick={prev}
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full border border-white/40 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:hidden"
|
||||
onClick={next}
|
||||
aria-label="Next slide"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
"use client";
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
type JsonLdSchemasProps = {
|
||||
lang: "es" | "en";
|
||||
};
|
||||
|
||||
const ORG_ID = "https://sitemente.com/#organization";
|
||||
const LOCAL_ID = "https://sitemente.com/#localbusiness";
|
||||
const SERVICES_ID = "https://sitemente.com/#services";
|
||||
const FAQ_ID = "https://sitemente.com/#faq";
|
||||
|
||||
export default function JsonLdSchemas({ lang }: JsonLdSchemasProps) {
|
||||
const isEs = lang === "es";
|
||||
|
||||
const organization = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"@id": ORG_ID,
|
||||
name: "SiteMente",
|
||||
url: "https://sitemente.com",
|
||||
logo: "https://sitemente.com/logo.png",
|
||||
description: isEs
|
||||
? "Agencia de implementación de IA para negocios locales en España. Especialistas en chatbots, automatización y sitios web inteligentes."
|
||||
: "AI implementation agency for local businesses in Spain. Specialists in chatbots, automation and smart websites.",
|
||||
email: "hola@sitemente.com",
|
||||
foundingDate: "2025",
|
||||
parentOrganization: {
|
||||
"@type": "Organization",
|
||||
name: "HolaCompi",
|
||||
},
|
||||
areaServed: {
|
||||
"@type": "Country",
|
||||
name: "España",
|
||||
},
|
||||
};
|
||||
|
||||
const localBusiness = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
"@id": LOCAL_ID,
|
||||
name: "SiteMente - Agencia de IA Málaga",
|
||||
url: "https://sitemente.com",
|
||||
priceRange: "€€€",
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressLocality: "Málaga",
|
||||
addressRegion: "Andalucía",
|
||||
addressCountry: "ES",
|
||||
},
|
||||
geo: {
|
||||
"@type": "GeoCoordinates",
|
||||
latitude: 36.7213,
|
||||
longitude: -4.4214,
|
||||
},
|
||||
areaServed: [
|
||||
{ "@type": "Place", name: "Málaga" },
|
||||
{ "@type": "Place", name: "Marbella" },
|
||||
{ "@type": "Place", name: "Benalmádena" },
|
||||
{ "@type": "Place", name: "Costa del Sol" },
|
||||
],
|
||||
parentOrganization: { "@id": ORG_ID },
|
||||
};
|
||||
|
||||
const serviceDescriptions = isEs
|
||||
? [
|
||||
{
|
||||
name: "Smart Starter - AI Chat + Reservas Automatizadas",
|
||||
description:
|
||||
"Chat IA para sitio web con reservas automatizadas, captura de leads y análisis básicos.",
|
||||
audience: "Restaurantes, clínicas, negocios locales",
|
||||
price: 299,
|
||||
},
|
||||
{
|
||||
name: "Smart Site - Web Inteligente Completa",
|
||||
description:
|
||||
"Sitio web Next.js completo con IA integrada que vende, reserva y da soporte en español e inglés.",
|
||||
audience: "Negocios listos para una web moderna",
|
||||
price: 749,
|
||||
},
|
||||
{
|
||||
name: "AI Growth Partner",
|
||||
description:
|
||||
"Partner estratégico de IA: multicanal (web, WhatsApp, llamadas), integraciones CRM.",
|
||||
audience: "Grupos de restaurantes, inmobiliarias, cadenas de alquiler",
|
||||
price: 1950,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: "Smart Starter - AI Chat + Automated Bookings",
|
||||
description:
|
||||
"AI website chat with automated bookings, lead capture, and basic analytics.",
|
||||
audience: "Restaurants, clinics, local businesses",
|
||||
price: 299,
|
||||
},
|
||||
{
|
||||
name: "Smart Site - Full Smart Website",
|
||||
description:
|
||||
"Complete Next.js website with integrated AI that sells, books, and supports in Spanish and English.",
|
||||
audience: "Businesses ready for a modern website",
|
||||
price: 749,
|
||||
},
|
||||
{
|
||||
name: "AI Growth Partner",
|
||||
description:
|
||||
"Strategic AI partner: multichannel (web, WhatsApp, calls), CRM integrations.",
|
||||
audience: "Restaurant groups, real estate teams, rental chains",
|
||||
price: 1950,
|
||||
},
|
||||
];
|
||||
|
||||
const services = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"@id": SERVICES_ID,
|
||||
itemListElement: serviceDescriptions.map((service, index) => ({
|
||||
"@type": "ListItem",
|
||||
position: index + 1,
|
||||
item: {
|
||||
"@type": "Service",
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
serviceAudience: service.audience,
|
||||
provider: { "@id": ORG_ID },
|
||||
areaServed: { "@type": "Country", name: "España" },
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
priceCurrency: "EUR",
|
||||
price: service.price,
|
||||
priceSpecification: {
|
||||
"@type": "UnitPriceSpecification",
|
||||
priceCurrency: "EUR",
|
||||
price: service.price,
|
||||
unitText: isEs ? "mes" : "month",
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
const faq = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"@id": FAQ_ID,
|
||||
mainEntity: isEs
|
||||
? [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "¿Qué es la implementación de IA para negocios?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "La implementación de IA para negocios significa instalar agentes inteligentes (chatbots, asistentes de voz, automatizaciones) en tu sitio web para que respondan preguntas, tomen reservas y vendan 24/7 sin intervención humana.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "¿Cuánto cuesta implementar IA en mi negocio en España?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Nuestros planes comienzan desde 900€ de instalación + 299€/mes para chat IA básico, hasta planes completos desde 3.500€ + 749€/mes con sitio web nuevo.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "¿Por qué necesito IA en mi sitio web en 2026?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "El 95% de las interacciones con clientes involucran IA. Los negocios con chatbots aumentan ventas en 67% de media y reducen costes de soporte en 30%.",
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "What is AI implementation for businesses?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "AI implementation for businesses means installing intelligent agents (chatbots, voice assistants, automations) on your website so they answer questions, take bookings, and sell 24/7 without human intervention.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How much does it cost to implement AI in my business in Spain?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Our plans start from a €900 setup + €299/month for basic AI chat, up to complete packages from €3,500 + €749/month with a new website.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Why do I need AI on my website in 2026?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "95% of customer interactions involve AI. Businesses with chatbots increase sales by an average of 67% and reduce support costs by 30%.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id="jsonld-organization"
|
||||
type="application/ld+json"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(organization) }}
|
||||
/>
|
||||
<Script
|
||||
id="jsonld-localbusiness"
|
||||
type="application/ld+json"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusiness) }}
|
||||
/>
|
||||
<Script
|
||||
id="jsonld-services"
|
||||
type="application/ld+json"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(services) }}
|
||||
/>
|
||||
<Script
|
||||
id="jsonld-faq"
|
||||
type="application/ld+json"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faq) }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type PricingTableProps = {
|
||||
lang: "es" | "en";
|
||||
onContact: () => void;
|
||||
};
|
||||
|
||||
type Plan = {
|
||||
id: string;
|
||||
name: string;
|
||||
setup: string;
|
||||
monthly: number | null;
|
||||
term: string;
|
||||
features: string[];
|
||||
optional: string[];
|
||||
cta: string;
|
||||
popular?: boolean;
|
||||
subtext?: string;
|
||||
};
|
||||
|
||||
const formatMonthly = (
|
||||
value: number,
|
||||
yearly: boolean,
|
||||
lang: PricingTableProps["lang"]
|
||||
) => {
|
||||
const discounted = yearly ? Math.round(value * 0.85) : value;
|
||||
return `${discounted} €/${lang === "es" ? "mes" : "mo"}`;
|
||||
};
|
||||
|
||||
export default function PricingTable({ lang, onContact }: PricingTableProps) {
|
||||
const [yearly, setYearly] = useState(false);
|
||||
const [showFairUse, setShowFairUse] = useState(false);
|
||||
|
||||
const pricingContent = useMemo(
|
||||
() => ({
|
||||
es: {
|
||||
labels: {
|
||||
popular: "Más popular",
|
||||
setup: "Instalación",
|
||||
billedAnnually: "(facturado anualmente)",
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
id: "starter",
|
||||
name: "Smart Starter",
|
||||
setup: "900 €",
|
||||
monthly: 299,
|
||||
term: "6-12 meses",
|
||||
features: [
|
||||
"Chat IA en la web (1 idioma)",
|
||||
"Reservas y captación de leads",
|
||||
"Bandeja ligera de leads",
|
||||
"Analíticas básicas",
|
||||
],
|
||||
optional: ["+WhatsApp (+100€/mes)", "+Idioma extra (+80€/mes)"],
|
||||
cta: "Empezar",
|
||||
},
|
||||
{
|
||||
id: "site",
|
||||
name: "Smart Site",
|
||||
setup: "3.500 €",
|
||||
monthly: 749,
|
||||
term: "6-12 meses",
|
||||
features: [
|
||||
"Todo lo incluido en Starter",
|
||||
"Nuevo sitio web Next.js",
|
||||
"WhatsApp incluido",
|
||||
"2 idiomas (ES+EN)",
|
||||
"Sugerencias de contenido con IA",
|
||||
"Analíticas avanzadas",
|
||||
],
|
||||
optional: [
|
||||
"+Llamadas de voz (+150€/mes)",
|
||||
"+Ubicación extra (+120€/mes)",
|
||||
],
|
||||
cta: "Empezar",
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "growth",
|
||||
name: "AI Growth Partner",
|
||||
setup: "5.000 €",
|
||||
monthly: 1950,
|
||||
term: "12 meses",
|
||||
features: [
|
||||
"Todo lo incluido en Smart Site",
|
||||
"Llamadas de voz incluidas",
|
||||
"Soporte multiubicación",
|
||||
"Integraciones CRM",
|
||||
"Automatizaciones personalizadas",
|
||||
"Llamada estratégica mensual",
|
||||
"Soporte prioritario",
|
||||
],
|
||||
optional: ["+Pack de contenido IA (+200€/mes)"],
|
||||
cta: "Empezar",
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
name: "Enterprise / A medida",
|
||||
setup: "Personalizado",
|
||||
monthly: null,
|
||||
term: "Definimos el alcance contigo",
|
||||
features: [
|
||||
"Todo a medida",
|
||||
"Multimarca y white-label",
|
||||
"Account manager dedicado",
|
||||
"SLA de rendimiento",
|
||||
],
|
||||
optional: [],
|
||||
cta: "Contactar",
|
||||
subtext: "Hablemos",
|
||||
},
|
||||
] satisfies Plan[],
|
||||
},
|
||||
en: {
|
||||
labels: {
|
||||
popular: "Most popular",
|
||||
setup: "Setup",
|
||||
billedAnnually: "(billed annually)",
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
id: "starter",
|
||||
name: "Smart Starter",
|
||||
setup: "900 €",
|
||||
monthly: 299,
|
||||
term: "6-12 months",
|
||||
features: [
|
||||
"AI website chat (1 language)",
|
||||
"Booking & lead capture",
|
||||
"Light lead inbox",
|
||||
"Basic analytics",
|
||||
],
|
||||
optional: ["+WhatsApp (+100€/mo)", "+Extra language (+80€/mo)"],
|
||||
cta: "Get started",
|
||||
},
|
||||
{
|
||||
id: "site",
|
||||
name: "Smart Site",
|
||||
setup: "3,500 €",
|
||||
monthly: 749,
|
||||
term: "6-12 months",
|
||||
features: [
|
||||
"Everything in Starter",
|
||||
"New Next.js website",
|
||||
"WhatsApp included",
|
||||
"2 languages (ES+EN)",
|
||||
"AI content suggestions",
|
||||
"Advanced analytics",
|
||||
],
|
||||
optional: ["+Voice calls (+150€/mo)", "+Extra location (+120€/mo)"],
|
||||
cta: "Get started",
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "growth",
|
||||
name: "AI Growth Partner",
|
||||
setup: "5,000 €",
|
||||
monthly: 1950,
|
||||
term: "12 months",
|
||||
features: [
|
||||
"Everything in Smart Site",
|
||||
"Voice calls included",
|
||||
"Multi-location support",
|
||||
"CRM integrations",
|
||||
"Custom automations",
|
||||
"Monthly strategy call",
|
||||
"Priority support",
|
||||
],
|
||||
optional: ["+AI content pack (+200€/mo)"],
|
||||
cta: "Get started",
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
name: "Enterprise / Custom",
|
||||
setup: "Custom",
|
||||
monthly: null,
|
||||
term: "Let us scope it",
|
||||
features: [
|
||||
"Everything tailored",
|
||||
"Multi-brand & white-label",
|
||||
"Dedicated account manager",
|
||||
"Performance SLAs",
|
||||
],
|
||||
optional: [],
|
||||
cta: "Contact us",
|
||||
subtext: "Let us talk",
|
||||
},
|
||||
] satisfies Plan[],
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const pricing = pricingContent[lang];
|
||||
const plans = pricing.plans;
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<div className="flex flex-col items-center justify-between gap-4 rounded-2xl border border-white/15 bg-white/10 p-5 text-center backdrop-blur md:flex-row md:text-left">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
|
||||
{lang === "es" ? "Pago anual" : "Pay yearly"}
|
||||
</p>
|
||||
<p className="mt-2 text-base text-white/90">
|
||||
{lang === "es"
|
||||
? "Paga anual y ahorra 15%"
|
||||
: "Pay yearly - Save 15%"}
|
||||
</p>
|
||||
</div>
|
||||
<label className="flex items-center gap-3 rounded-full border border-white/25 bg-white/10 px-4 py-2 text-sm font-semibold text-white">
|
||||
<span>{lang === "es" ? "Descuento 15%" : "Save 15%"}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setYearly((prev) => !prev)}
|
||||
className={`relative h-6 w-12 rounded-full transition ${
|
||||
yearly ? "bg-brand-pink" : "bg-white/20"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute left-1 top-1 h-4 w-4 rounded-full bg-white transition ${
|
||||
yearly ? "translate-x-6" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-4 items-stretch">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={`flex h-full flex-col rounded-2xl border p-6 backdrop-blur transition duration-300 hover:-translate-y-2 hover:shadow-[0_25px_50px_-20px_rgba(118,75,162,0.65)] ${
|
||||
plan.popular
|
||||
? "border-brand-pink bg-white/12"
|
||||
: "border-white/15 bg-white/8"
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<span className="inline-flex rounded-full bg-brand-pink/20 px-3 py-1 text-xs font-semibold text-white">
|
||||
{pricing.labels.popular}
|
||||
</span>
|
||||
)}
|
||||
<h3 className="mt-4 text-xl font-semibold">{plan.name}</h3>
|
||||
<p className="mt-2 text-sm text-white/70">
|
||||
{pricing.labels.setup}: {plan.setup}
|
||||
</p>
|
||||
<p className="mt-6 text-3xl font-bold">
|
||||
{plan.monthly === null
|
||||
? plan.subtext
|
||||
: formatMonthly(plan.monthly, yearly, lang)}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-white/70">
|
||||
{plan.term}
|
||||
{plan.monthly !== null && yearly && (
|
||||
<span className="block text-xs text-white/60">
|
||||
{pricing.labels.billedAnnually}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<ul className="mt-6 space-y-2 text-sm text-white/90">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature}>• {feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
{plan.optional.length > 0 && (
|
||||
<div className="mt-4 text-xs text-white/60">
|
||||
{plan.optional.map((option) => (
|
||||
<p key={option}>{option}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={onContact}
|
||||
className="mt-auto w-full rounded-lg bg-brand-pink px-4 py-2 text-sm font-semibold text-white transition hover:-translate-y-1 hover:bg-[#ff7bc0]"
|
||||
>
|
||||
{plan.cta}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-xs text-white/70">
|
||||
Todos los planes incluyen un uso generoso de IA para un negocio típico.
|
||||
Se aplica una{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFairUse(true)}
|
||||
className="font-semibold text-white underline decoration-white/60 underline-offset-4"
|
||||
>
|
||||
política de uso justo
|
||||
</button>
|
||||
, sin cargos sorpresa.
|
||||
</p>
|
||||
|
||||
{showFairUse && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center px-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setShowFairUse(false)}
|
||||
/>
|
||||
<div className="relative z-10 w-full max-w-2xl rounded-2xl border border-white/20 bg-white/10 p-8 text-white shadow-purple-soft backdrop-blur">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.3em] text-white/70">
|
||||
Uso justo
|
||||
</p>
|
||||
<h3 className="mt-3 text-2xl font-bold">
|
||||
Qué significa "uso justo"?
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFairUse(false)}
|
||||
className="rounded-full border border-white/40 px-3 py-1 text-xs font-semibold text-white/80 transition hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6 space-y-4 text-sm text-white/85">
|
||||
<p>
|
||||
Cada plan incluye suficientes mensajes de IA, llamadas y
|
||||
conversaciones de WhatsApp para un negocio típico de tu tamaño.
|
||||
</p>
|
||||
<p>
|
||||
Si tu uso se mantiene muy por encima de lo normal (por ejemplo,
|
||||
porque añades nuevas marcas, locales o campañas), haremos lo
|
||||
siguiente:
|
||||
</p>
|
||||
<ol className="list-decimal space-y-2 pl-5">
|
||||
<li>Te avisaremos primero.</li>
|
||||
<li>Te enviaremos un informe sencillo de uso.</li>
|
||||
<li>
|
||||
Te propondremos opciones (seguir igual con algunos límites o
|
||||
subir de plan / añadir más capacidad).
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
Nunca aplicamos cargos extra sin hablar contigo antes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import Head from "next/head";
|
||||
|
||||
type SeoMetaProps = {
|
||||
lang: "es" | "en";
|
||||
};
|
||||
|
||||
export default function SeoMeta({ lang }: SeoMetaProps) {
|
||||
const isEs = lang === "es";
|
||||
const title = isEs
|
||||
? "SiteMente | Implementación de IA Málaga - Chatbots para Restaurantes, Inmobiliarias, Rent a Car"
|
||||
: "SiteMente | AI Implementation Málaga - Chatbots for Restaurants, Real Estate, Car Rentals";
|
||||
const description = isEs
|
||||
? "Agencia de IA en Málaga. Chatbots y automatización para restaurantes, inmobiliarias y alquiler de coches desde €299/mes. Soporte bilingüe ES/EN. +120 clientes en Costa del Sol."
|
||||
: "AI agency in Málaga. Chatbots and automation for restaurants, real estate and car rentals from €299/month. Bilingual support ES/EN. +120 clients in Costa del Sol.";
|
||||
const keywords = isEs
|
||||
? "implementación IA España, chatbot restaurantes Málaga, IA inmobiliarias Costa del Sol, automatización negocios Marbella, agencia IA España"
|
||||
: "AI implementation Spain, restaurant chatbot Málaga, real estate AI Costa del Sol, business automation Marbella";
|
||||
const url = isEs ? "https://sitemente.com" : "https://sitemente.com/en";
|
||||
const locale = isEs ? "es_ES" : "en_US";
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta
|
||||
name="googlebot"
|
||||
content="index, follow, max-image-preview:large"
|
||||
/>
|
||||
<link rel="canonical" href={url} />
|
||||
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:image" content="https://sitemente.com/og-banner.jpg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:locale" content={locale} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="SiteMente" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://sitemente.com/og-banner.jpg"
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type ServicesAndPricingProps = {
|
||||
lang: "es" | "en";
|
||||
onContact: () => void;
|
||||
};
|
||||
|
||||
type ServiceCard = {
|
||||
id: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
for: string;
|
||||
description: string[];
|
||||
sections?: { title: string; items: string[] }[];
|
||||
setup: string;
|
||||
monthly: number | null;
|
||||
term: string;
|
||||
features: string[];
|
||||
optional: string[];
|
||||
cta: string;
|
||||
popular?: boolean;
|
||||
subtext?: string;
|
||||
};
|
||||
|
||||
const fadeUp = {
|
||||
hidden: { opacity: 0, y: 24 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
const content = {
|
||||
es: {
|
||||
sectionTitle: "Servicios y Precios",
|
||||
sectionEyebrow: "Servicios y Precios",
|
||||
headline: "Convertimos tu web en un vendedor inteligente que trabaja 24/7",
|
||||
subheadline:
|
||||
"En 2026, los clientes esperan respuestas instantáneas, reservas sin fricción y experiencias personalizadas. Si tu web no puede hacerlo, están yendo a tu competencia.",
|
||||
yearlyLabel: "Pago anual",
|
||||
yearlyDesc: "Paga anual y ahorra 15%",
|
||||
saveLabel: "Descuento 15%",
|
||||
setupLabel: "Instalación",
|
||||
fromLabel: "Desde",
|
||||
perMonth: "/mes",
|
||||
billedAnnually: "(facturado anualmente)",
|
||||
examplesTitle: "Casos de uso reales en Costa del Sol",
|
||||
examplesCta: "Descubre tu plan ideal",
|
||||
fairUseNote: "Todos los planes incluyen un uso generoso de IA para un negocio típico. Se aplica una",
|
||||
fairUseLink: "política de uso justo",
|
||||
fairUseEnd: ", sin cargos sorpresa.",
|
||||
fairUseTitle: "Uso justo",
|
||||
fairUseQuestion: 'Qué significa "uso justo"?',
|
||||
close: "Cerrar",
|
||||
fairUseBody: [
|
||||
"Cada plan incluye suficientes mensajes de IA, llamadas y conversaciones de WhatsApp para un negocio típico de tu tamaño.",
|
||||
"Si tu uso se mantiene muy por encima de lo normal (por ejemplo, porque añades nuevas marcas, locales o campañas), haremos lo siguiente:",
|
||||
],
|
||||
fairUseList: [
|
||||
"Te avisaremos primero.",
|
||||
"Te enviaremos un informe sencillo de uso.",
|
||||
"Te propondremos opciones (seguir igual con algunos límites o subir de plan / añadir más capacidad).",
|
||||
],
|
||||
fairUseEndBody: "Nunca aplicamos cargos extra sin hablar contigo antes.",
|
||||
popular: "Más popular",
|
||||
},
|
||||
en: {
|
||||
sectionTitle: "Services & Pricing",
|
||||
sectionEyebrow: "Services & Pricing",
|
||||
headline: "Turn your website into a 24/7 intelligent salesperson",
|
||||
subheadline:
|
||||
"In 2026, customers expect instant answers, frictionless bookings, and personalized experiences. If your site can't deliver, they're going to your competitors.",
|
||||
yearlyLabel: "Pay yearly",
|
||||
yearlyDesc: "Pay yearly - Save 15%",
|
||||
saveLabel: "Save 15%",
|
||||
setupLabel: "Setup",
|
||||
fromLabel: "From",
|
||||
perMonth: "/month",
|
||||
billedAnnually: "(billed annually)",
|
||||
examplesTitle: "Real use cases on the Costa del Sol",
|
||||
examplesCta: "Find your ideal plan",
|
||||
fairUseNote: "All plans include generous AI usage for a typical business. A",
|
||||
fairUseLink: "fair use policy",
|
||||
fairUseEnd: " applies, no surprise charges.",
|
||||
fairUseTitle: "Fair use",
|
||||
fairUseQuestion: 'What does "fair use" mean?',
|
||||
close: "Close",
|
||||
fairUseBody: [
|
||||
"Each plan includes enough AI messages, calls and WhatsApp conversations for a typical business of your size.",
|
||||
"If your usage stays well above normal (e.g. you add new brands, locations or campaigns), we will:",
|
||||
],
|
||||
fairUseList: [
|
||||
"Notify you first.",
|
||||
"Send you a simple usage report.",
|
||||
"Propose options (continue as-is with some limits, or upgrade / add capacity).",
|
||||
],
|
||||
fairUseEndBody: "We never apply extra charges without speaking to you first.",
|
||||
popular: "Most popular",
|
||||
},
|
||||
} as const;
|
||||
|
||||
const serviceCards: Record<"es" | "en", ServiceCard[]> = {
|
||||
es: [
|
||||
{
|
||||
id: "starter",
|
||||
icon: "💬",
|
||||
title: "AI Chat + Reservas Automatizadas",
|
||||
for: "Restaurantes, clínicas, negocios locales con web existente",
|
||||
description: [
|
||||
"¿Tu web pierde clientes fuera de horario? Tu sitio responde preguntas, toma reservas y captura leads mientras duermes.",
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "Resultados típicos:",
|
||||
items: [
|
||||
"24/7 reservas sin intervención humana",
|
||||
"Reduce llamadas repetitivas en un 70%",
|
||||
"ROI positivo en el primer mes",
|
||||
],
|
||||
},
|
||||
],
|
||||
setup: "900 €",
|
||||
monthly: 299,
|
||||
term: "6-12 meses",
|
||||
features: [
|
||||
"Chat IA en la web (1 idioma)",
|
||||
"Reservas y captación de leads",
|
||||
"Analíticas básicas",
|
||||
],
|
||||
optional: ["+WhatsApp (+100€/mes)", "+Idioma extra (+80€/mes)"],
|
||||
cta: "Empezar",
|
||||
},
|
||||
{
|
||||
id: "site",
|
||||
icon: "🌐",
|
||||
title: "Web Inteligente Completa",
|
||||
for: "Negocios listos para una web moderna + cerebro IA",
|
||||
description: [
|
||||
"Construimos tu nuevo sitio web Next.js (rápido, seguro, SEO) e integramos IA que vende, reserva y da soporte en español e inglés.",
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "Incluye:",
|
||||
items: [
|
||||
"Sitio que convierte visitas en ventas",
|
||||
"Chat + WhatsApp incluidos",
|
||||
"2 idiomas desde el día uno",
|
||||
"Analíticas avanzadas",
|
||||
],
|
||||
},
|
||||
],
|
||||
setup: "3.500 €",
|
||||
monthly: 749,
|
||||
term: "6-12 meses",
|
||||
features: [
|
||||
"Todo lo de Starter",
|
||||
"Nuevo sitio web Next.js",
|
||||
"WhatsApp incluido",
|
||||
"2 idiomas (ES+EN)",
|
||||
],
|
||||
optional: ["+Llamadas de voz (+150€/mes)", "+Ubicación extra (+120€/mes)"],
|
||||
cta: "Empezar",
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "growth",
|
||||
icon: "🚀",
|
||||
title: "Partner Estratégico de IA",
|
||||
for: "Grupos de restaurantes, inmobiliarias, cadenas de alquiler",
|
||||
description: [
|
||||
"Socio estratégico: IA multicanal (web, WhatsApp, llamadas), integraciones CRM y consultoría mensual.",
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "Incluye:",
|
||||
items: [
|
||||
"Todo lo de Smart Site",
|
||||
"Llamadas de voz incluidas",
|
||||
"Soporte multiubicación",
|
||||
"Integraciones CRM",
|
||||
"Llamada estratégica mensual",
|
||||
],
|
||||
},
|
||||
],
|
||||
setup: "5.000 €",
|
||||
monthly: 1950,
|
||||
term: "12 meses",
|
||||
features: [
|
||||
"Todo lo de Smart Site",
|
||||
"Llamadas de voz incluidas",
|
||||
"Integraciones CRM",
|
||||
"Soporte prioritario",
|
||||
],
|
||||
optional: ["+Pack contenido IA (+200€/mes)"],
|
||||
cta: "Hablemos",
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
icon: "🎯",
|
||||
title: "Enterprise / A medida",
|
||||
for: "Operaciones a escala con necesidades custom",
|
||||
description: [
|
||||
"Todo adaptado a tu operación: multimarca, white-label, account manager dedicado y SLA de rendimiento.",
|
||||
],
|
||||
setup: "Personalizado",
|
||||
monthly: null,
|
||||
term: "Definimos el alcance contigo",
|
||||
features: [
|
||||
"Todo a medida",
|
||||
"Multimarca y white-label",
|
||||
"Account manager dedicado",
|
||||
"SLA de rendimiento",
|
||||
],
|
||||
optional: [],
|
||||
cta: "Contactar",
|
||||
subtext: "Hablemos",
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
id: "starter",
|
||||
icon: "💬",
|
||||
title: "AI Chat + Automated Bookings",
|
||||
for: "Restaurants, clinics, local businesses with existing websites",
|
||||
description: [
|
||||
"Is your website losing customers after hours? Your site answers questions, takes bookings, and captures leads while you sleep.",
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "Typical results:",
|
||||
items: [
|
||||
"24/7 bookings without human intervention",
|
||||
"Reduce repetitive calls by 70%",
|
||||
"Positive ROI in the first month",
|
||||
],
|
||||
},
|
||||
],
|
||||
setup: "900 €",
|
||||
monthly: 299,
|
||||
term: "6-12 months",
|
||||
features: [
|
||||
"AI website chat (1 language)",
|
||||
"Booking & lead capture",
|
||||
"Basic analytics",
|
||||
],
|
||||
optional: ["+WhatsApp (+100€/mo)", "+Extra language (+80€/mo)"],
|
||||
cta: "Get started",
|
||||
},
|
||||
{
|
||||
id: "site",
|
||||
icon: "🌐",
|
||||
title: "Full Smart Website",
|
||||
for: "Businesses ready for a modern site + AI brain",
|
||||
description: [
|
||||
"We build your new Next.js website (fast, secure, SEO) and integrate AI that sells, books, and supports in Spanish and English.",
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "Includes:",
|
||||
items: [
|
||||
"Site that converts visits into sales",
|
||||
"Chat + WhatsApp included",
|
||||
"2 languages from day one",
|
||||
"Advanced analytics",
|
||||
],
|
||||
},
|
||||
],
|
||||
setup: "3,500 €",
|
||||
monthly: 749,
|
||||
term: "6-12 months",
|
||||
features: [
|
||||
"Everything in Starter",
|
||||
"New Next.js website",
|
||||
"WhatsApp included",
|
||||
"2 languages (ES+EN)",
|
||||
],
|
||||
optional: ["+Voice calls (+150€/mo)", "+Extra location (+120€/mo)"],
|
||||
cta: "Get started",
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "growth",
|
||||
icon: "🚀",
|
||||
title: "AI Strategy Partner",
|
||||
for: "Restaurant groups, real estate teams, rental chains",
|
||||
description: [
|
||||
"Strategic partner: multichannel AI (web, WhatsApp, calls), CRM integrations, monthly consulting.",
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "Includes:",
|
||||
items: [
|
||||
"Everything in Smart Site",
|
||||
"Voice calls included",
|
||||
"Multi-location support",
|
||||
"CRM integrations",
|
||||
"Monthly strategy call",
|
||||
],
|
||||
},
|
||||
],
|
||||
setup: "5,000 €",
|
||||
monthly: 1950,
|
||||
term: "12 months",
|
||||
features: [
|
||||
"Everything in Smart Site",
|
||||
"Voice calls included",
|
||||
"CRM integrations",
|
||||
"Priority support",
|
||||
],
|
||||
optional: ["+AI content pack (+200€/mo)"],
|
||||
cta: "Let's talk",
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
icon: "🎯",
|
||||
title: "Enterprise / Custom",
|
||||
for: "Large-scale operations with custom needs",
|
||||
description: [
|
||||
"Everything tailored: multi-brand, white-label, dedicated account manager, performance SLA.",
|
||||
],
|
||||
setup: "Custom",
|
||||
monthly: null,
|
||||
term: "Let us scope it",
|
||||
features: [
|
||||
"Everything tailored",
|
||||
"Multi-brand & white-label",
|
||||
"Dedicated account manager",
|
||||
"Performance SLAs",
|
||||
],
|
||||
optional: [],
|
||||
cta: "Contact us",
|
||||
subtext: "Let's talk",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const examples = {
|
||||
es: [
|
||||
{ title: "🍽️ Restaurantes", text: "Reservas a las 3 AM, respuestas sobre alérgenos, sugerencias de platos." },
|
||||
{ title: "🏠 Inmobiliarias", text: "IA responde dudas, agenda visitas, cualifica leads." },
|
||||
{ title: "🚗 Rent a Car", text: "Cotiza y reserva en 3 idiomas sin intervención humana." },
|
||||
],
|
||||
en: [
|
||||
{ title: "🍽️ Restaurants", text: "Reservations at 3 AM, allergy answers, dish suggestions." },
|
||||
{ title: "🏠 Real Estate", text: "AI answers questions, books viewings, qualifies leads." },
|
||||
{ title: "🚗 Rent a Car", text: "Quote and book in 3 languages with no human intervention." },
|
||||
],
|
||||
};
|
||||
|
||||
export default function ServicesAndPricing({
|
||||
lang,
|
||||
onContact,
|
||||
}: ServicesAndPricingProps) {
|
||||
const [yearly, setYearly] = useState(false);
|
||||
const [showFairUse, setShowFairUse] = useState(false);
|
||||
|
||||
const t = content[lang];
|
||||
const cards = serviceCards[lang];
|
||||
|
||||
const formatMonthly = (value: number) => {
|
||||
const price = yearly ? Math.round(value * 0.85) : value;
|
||||
return `${price} €${t.perMonth}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="services"
|
||||
className="mx-auto w-full max-w-6xl px-6 py-20"
|
||||
>
|
||||
<motion.div
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.7 }}
|
||||
>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
|
||||
{t.sectionEyebrow}
|
||||
</p>
|
||||
<h2 className="mt-4 text-3xl font-bold sm:text-4xl">
|
||||
{t.sectionTitle}
|
||||
</h2>
|
||||
<p className="mt-4 max-w-3xl text-base text-white/85">
|
||||
{t.subheadline}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="mt-6 flex flex-col items-center justify-between gap-4 rounded-2xl border border-white/15 bg-white/10 p-5 text-center backdrop-blur sm:flex-row">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white/90">{t.yearlyLabel}</p>
|
||||
<p className="mt-1 text-sm text-white/75">{t.yearlyDesc}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setYearly((prev) => !prev)}
|
||||
className="flex items-center gap-3 rounded-full border border-white/25 bg-white/10 px-4 py-2 text-sm font-semibold text-white"
|
||||
>
|
||||
<span>{t.saveLabel}</span>
|
||||
<span
|
||||
className={`relative block h-6 w-11 rounded-full transition ${
|
||||
yearly ? "bg-brand-pink" : "bg-white/20"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute left-1 top-1 h-4 w-4 rounded-full bg-white transition ${
|
||||
yearly ? "translate-x-5" : ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-2 xl:grid-cols-4">
|
||||
{cards.map((card, index) => (
|
||||
<motion.div
|
||||
key={card.id}
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.08 }}
|
||||
className={`flex flex-col rounded-2xl border p-6 backdrop-blur transition hover:border-white/30 ${
|
||||
card.popular
|
||||
? "border-brand-pink bg-white/12 shadow-purple-soft"
|
||||
: "border-white/15 bg-white/8"
|
||||
}`}
|
||||
>
|
||||
{card.popular && (
|
||||
<span className="mb-3 inline-flex w-fit rounded-full bg-brand-pink/25 px-3 py-0.5 text-xs font-semibold text-white">
|
||||
{t.popular}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{card.icon}</span>
|
||||
<h3 className="text-lg font-semibold">{card.title}</h3>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-white/70">{card.for}</p>
|
||||
|
||||
<div className="mt-4 space-y-2 text-sm text-white/90">
|
||||
{card.description.map((p, i) => (
|
||||
<p key={i}>{p}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{card.sections?.map((section) => (
|
||||
<div key={section.title} className="mt-4">
|
||||
<p className="text-xs font-semibold text-white/85">
|
||||
{section.title}
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1 text-xs text-white/80">
|
||||
{section.items.map((item) => (
|
||||
<li key={item}>• {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mt-6 rounded-xl border border-white/15 bg-white/5 px-4 py-3">
|
||||
<p className="text-xs text-white/70">
|
||||
{t.setupLabel}: <span className="font-semibold text-white">{card.setup}</span>
|
||||
</p>
|
||||
<p className="mt-1 text-xl font-bold text-brand-pink">
|
||||
{card.monthly === null
|
||||
? card.subtext
|
||||
: card.monthly !== null
|
||||
? `${t.fromLabel} ${formatMonthly(card.monthly)}`
|
||||
: null}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-white/60">
|
||||
{card.term}
|
||||
{card.monthly !== null && yearly && (
|
||||
<span className="block">{t.billedAnnually}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="mt-4 space-y-1.5 text-sm text-white/85">
|
||||
{card.features.map((f) => (
|
||||
<li key={f}>✓ {f}</li>
|
||||
))}
|
||||
</ul>
|
||||
{card.optional.length > 0 && (
|
||||
<p className="mt-3 text-xs text-white/60">
|
||||
{card.optional.join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onContact}
|
||||
className="mt-auto mt-6 w-full rounded-lg bg-brand-pink py-2.5 text-sm font-semibold text-white transition hover:bg-[#ff7bc0]"
|
||||
>
|
||||
{card.cta}
|
||||
</button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-16 rounded-2xl border border-white/15 bg-white/10 p-8 backdrop-blur">
|
||||
<h3 className="text-xl font-semibold">{t.examplesTitle}</h3>
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-3">
|
||||
{examples[lang].map((ex) => (
|
||||
<div
|
||||
key={ex.title}
|
||||
className="rounded-xl border border-white/10 bg-white/5 p-4"
|
||||
>
|
||||
<p className="text-lg">{ex.title}</p>
|
||||
<p className="mt-2 text-sm text-white/80">"{ex.text}"</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center">
|
||||
<button
|
||||
onClick={onContact}
|
||||
className="rounded-lg bg-brand-pink px-6 py-3 text-sm font-semibold text-white transition hover:bg-[#ff7bc0]"
|
||||
>
|
||||
{t.examplesCta}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-xs text-white/70">
|
||||
{t.fairUseNote}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFairUse(true)}
|
||||
className="font-semibold text-white underline decoration-white/60 underline-offset-2"
|
||||
>
|
||||
{t.fairUseLink}
|
||||
</button>
|
||||
{t.fairUseEnd}
|
||||
</p>
|
||||
|
||||
{showFairUse && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60"
|
||||
onClick={() => setShowFairUse(false)}
|
||||
/>
|
||||
<div className="relative z-10 w-full max-w-lg rounded-2xl border border-white/20 bg-[#4a3572] p-8 text-white shadow-xl">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-widest text-white/70">
|
||||
{t.fairUseTitle}
|
||||
</p>
|
||||
<h3 className="mt-2 text-xl font-bold">{t.fairUseQuestion}</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFairUse(false)}
|
||||
className="rounded-full border border-white/40 px-3 py-1 text-xs font-semibold text-white/80 hover:bg-white/10"
|
||||
>
|
||||
{t.close}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6 space-y-3 text-sm text-white/85">
|
||||
{t.fairUseBody.map((p) => (
|
||||
<p key={p}>{p}</p>
|
||||
))}
|
||||
<ol className="list-decimal space-y-2 pl-5">
|
||||
{t.fairUseList.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ol>
|
||||
<p className="font-medium">{t.fairUseEndBody}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user