Add LICENSE, README, and Docs tab to Mission Control

This commit is contained in:
root
2026-02-22 07:33:18 +00:00
parent 3e7b457d5f
commit 0817444dc5
68 changed files with 6677 additions and 1673 deletions
+84
View File
@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const commandFile = path.join(process.cwd(), 'task-history.json')
export async function GET(request: NextRequest) {
try {
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const { searchParams } = new URL(request.url)
const project = searchParams.get('project')
if (project && project !== 'all') {
history = history.filter((h: any) => h.project === project)
}
return NextResponse.json({ history })
} catch (error) {
console.error('Error reading history:', error)
return NextResponse.json({ history: [] })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { task, command, project, action } = body
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const entry = {
id: Date.now().toString(),
task,
command,
project: project || 'sitemente',
reply: '',
action: action || 'task',
createdAt: new Date().toISOString(),
status: 'pending',
notified: false
}
history.push(entry)
// Keep only last 50 entries
if (history.length > 50) {
history = history.slice(-50)
}
fs.writeFileSync(commandFile, JSON.stringify(history, null, 2))
// Send Telegram notification to Horus
try {
const tgMessage = `📬 *New Task from MC*\n\n*Project:* ${project || 'sitemente'}\n*Task:* ${task}\n*Command:* ${command}\n\n_Reply via /api/command-reply/{id}_`
// Use message tool to notify (will work if Telegram is configured)
const { spawn } = require('child_process')
spawn('curl', ['-s', '-X', 'POST',
'http://localhost:3000/api/messages/send',
'-H', 'Content-Type: application/json',
'-d', JSON.stringify({
channel: 'telegram',
target: '382315644',
message: tgMessage
})
], { detached: true, stdio: 'ignore' })
} catch (e) {
console.log('Telegram notification skipped')
}
return NextResponse.json({ success: true, entry })
} catch (error) {
console.error('Error saving history:', error)
return NextResponse.json({ error: 'Failed to save' }, { status: 500 })
}
}
+61
View File
@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const commandFile = path.join(process.cwd(), 'task-history.json')
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json({ error: 'Missing id' }, { status: 400 })
}
try {
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const entry = history.find((h: any) => h.id === id)
if (!entry) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json({ entry })
} catch (error) {
return NextResponse.json({ error: 'Failed to get' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { id, reply } = body
if (!id || !reply) {
return NextResponse.json({ error: 'Missing id or reply' }, { status: 400 })
}
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const index = history.findIndex((h: any) => h.id === id)
if (index === -1) {
return NextResponse.json({ error: 'Entry not found' }, { status: 404 })
}
history[index].reply = reply
history[index].status = 'replied'
history[index].repliedAt = new Date().toISOString()
fs.writeFileSync(commandFile, JSON.stringify(history, null, 2))
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to save reply' }, { status: 500 })
}
}
+58
View File
@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { command, task, action } = body
if (!command || !task) {
return NextResponse.json(
{ error: 'Missing command or task' },
{ status: 400 }
)
}
// Log the command - in production this would trigger Horus
console.log(`[Task Command] Task: ${task}, Command: ${command}, Action: ${action}`)
// Store command in a file for Horus to pick up
const fs = require('fs')
const path = require('path')
const commandFile = path.join(process.cwd(), 'pending-commands.json')
let commands = []
if (fs.existsSync(commandFile)) {
commands = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
commands.push({
id: Date.now().toString(),
task,
command,
action,
createdAt: new Date().toISOString(),
status: 'pending'
})
fs.writeFileSync(commandFile, JSON.stringify(commands, null, 2))
return NextResponse.json({
success: true,
message: `Command sent for task: ${task}`,
commandId: Date.now().toString()
})
} catch (error) {
console.error('Error processing command:', error)
return NextResponse.json(
{ error: 'Failed to process command' },
{ status: 500 }
)
}
}
export async function GET() {
return NextResponse.json({
status: 'ok',
message: 'Command API ready'
})
}
+39 -10
View File
@@ -3,24 +3,53 @@ import { runSiteMenteVoiceTurn } from "../../../../lib/ai/siteMenteAgent";
export const runtime = "nodejs";
// Vapi webhook payload types
interface VapiMessage {
type: string;
role?: string;
transcript?: string;
}
interface VapiCall {
id: string;
}
interface VapiWebhookPayload {
message: VapiMessage;
call: VapiCall;
}
export async function POST(request: Request) {
try {
const body = (await request.json()) as { transcript?: string };
if (!body.transcript || typeof body.transcript !== "string") {
return NextResponse.json(
{ error: "transcript is required." },
{ status: 400 }
);
const body = (await request.json()) as VapiWebhookPayload;
// Extract transcript from Vapi's format
const message = body.message;
// Only process final transcripts
if (!message || message.type !== "transcript" || !message.transcript) {
// Return empty response for non-transcript messages
return NextResponse.json({ results: [] });
}
const transcript = message.transcript;
// Call MiniMax brain
const response = await runSiteMenteVoiceTurn({
transcript: body.transcript,
transcript: transcript,
});
// Return in Vapi's expected format
return NextResponse.json({
results: [
{
result: response.reply,
},
],
});
return NextResponse.json(response);
} catch (error) {
console.error("[SiteMente][API] Voice route failed", error);
console.error("[SiteMente][Vapi] Voice route failed", error);
return NextResponse.json(
{ error: "Failed to generate voice response." },
{ status: 500 }
+84
View File
@@ -0,0 +1,84 @@
import { NextResponse } from "next/server";
const STRIPE_KEY = process.env.STRIPE_SECRET_KEY || "";
const PLANS: Record<string, { name: string; price: number; currency: string }> = {
"starter-setup": { name: "AI Chat Setup", price: 90000, currency: "eur" },
"starter-monthly": { name: "AI Chat Monthly", price: 29900, currency: "eur" },
"site-setup": { name: "Smart Site Setup", price: 350000, currency: "eur" },
"site-monthly": { name: "Smart Site Monthly", price: 74900, currency: "eur" },
"growth-setup": { name: "AI Growth Setup", price: 500000, currency: "eur" },
"growth-monthly": { name: "AI Growth Monthly", price: 195000, currency: "eur" },
"demo-real-estate-essential": { name: "Real Estate Essential", price: 39000, currency: "eur" },
"demo-real-estate-profesional": { name: "Real Estate Professional", price: 79000, currency: "eur" },
"demo-real-estate-premium": { name: "Real Estate Premium", price: 139000, currency: "eur" },
"demo-restaurant-essential": { name: "Restaurant Essential", price: 39000, currency: "eur" },
"demo-restaurant-profesional": { name: "Restaurant Professional", price: 79000, currency: "eur" },
"demo-restaurant-premium": { name: "Restaurant Premium", price: 139000, currency: "eur" },
"demo-clinic-essential": { name: "Clinic Essential", price: 39000, currency: "eur" },
"demo-clinic-profesional": { name: "Clinic Professional", price: 79000, currency: "eur" },
"demo-clinic-premium": { name: "Clinic Premium", price: 139000, currency: "eur" },
"demo-home-services-essential": { name: "Home Services Essential", price: 39000, currency: "eur" },
"demo-home-services-profesional": { name: "Home Services Professional", price: 79000, currency: "eur" },
"demo-home-services-premium": { name: "Home Services Premium", price: 139000, currency: "eur" },
};
export async function POST(request: Request) {
try {
const body = await request.json();
const { planId, email, name, planType = "monthly" } = body;
const origin = request.headers.get("origin") || "http://45.95.42.114:1284";
let planKey = planId;
if (!planId.startsWith("demo-") && !PLANS[planId]) {
planKey = `${planId}-${planType}`;
}
const plan = PLANS[planKey];
if (!plan) {
return NextResponse.json({ error: "Invalid plan selected" }, { status: 400 });
}
const params = new URLSearchParams();
params.append("payment_method_types[]", "card");
params.append("line_items[0][price_data][currency]", plan.currency);
params.append("line_items[0][price_data][product_data][name]", plan.name);
params.append("line_items[0][price_data][product_data][description]", `SiteMente - ${plan.name}`);
params.append("line_items[0][price_data][unit_amount]", String(plan.price));
params.append("line_items[0][quantity]", "1");
params.append("mode", planType === "monthly" ? "subscription" : "payment");
params.append("customer_email", email || "");
params.append("metadata[customerName]", name || "");
params.append("metadata[planId]", planId);
params.append("metadata[planType]", planType);
params.append("success_url", `${origin}/success?session_id={CHECKOUT_SESSION_ID}`);
params.append("cancel_url", `${origin}/?cancelled=true`);
if (planType === "monthly") {
params.append("line_items[0][price_data][recurring][interval]", "month");
}
const response = await fetch("https://api.stripe.com/v1/checkout/sessions", {
method: "POST",
headers: {
"Authorization": `Bearer ${STRIPE_KEY}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: params.toString(),
});
const session = await response.json();
if (session.error) {
return NextResponse.json({ error: session.error.message }, { status: 400 });
}
return NextResponse.json({ sessionId: session.id, url: session.url });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Failed to create checkout session";
console.error("Stripe error:", message);
return NextResponse.json({ error: message }, { status: 500 });
}
}
+85
View File
@@ -0,0 +1,85 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const dataFile = path.join(process.cwd(), 'trading-traders.json')
const defaultTraders = [
{
id: 'dopetrades',
name: 'DopeTrades',
status: 'learning',
framesAnalyzed: 382,
patterns: ['Double Top/Bottom', 'Head & Shoulders', 'Triangles', 'Flags', 'Wedges'],
entryRules: [
'Identify clear structure (swing highs/lows)',
'Wait for retest of level',
'Confirm momentum in desired direction',
'Higher timeframe alignment',
'Entry on break of structure or retest',
'Confirmation candle required'
],
exitRules: [
'Stop below recent swing low (long)',
'Take profit minimum 2:1',
'Scale 50% at 1:1',
'Trailing stop after 1:1 achieved',
'Never move stop loss further'
],
indicators: [
'9 EMA (short term)',
'20 EMA (medium term)',
'50 SMA (trend filter)',
'RSI 14 (momentum)',
'Volume profile'
],
riskParams: [
'Max 2% risk per trade',
'Max 3 concurrent trades',
'6% daily max loss',
'10% weekly max loss',
'Stop after 3 losses'
],
timeframe: 'Multi: 4H/Daily trend, 1H structure, 15min entries',
notes: 'Frame analysis: 3% bullish, 7% bearish, 90% neutral. Dark charts confirmed.'
}
]
export async function GET() {
try {
let traders = defaultTraders
if (fs.existsSync(dataFile)) {
const saved = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
if (saved.length > 0) {
traders = saved
}
} else {
fs.writeFileSync(dataFile, JSON.stringify(defaultTraders, null, 2))
}
return NextResponse.json({ traders })
} catch (error) {
return NextResponse.json({ traders: defaultTraders })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
let traders = defaultTraders
if (fs.existsSync(dataFile)) {
traders = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
}
traders.push({
...body,
id: body.name.toLowerCase().replace(/\s+/g, '-'),
createdAt: new Date().toISOString()
})
fs.writeFileSync(dataFile, JSON.stringify(traders, null, 2))
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to save' }, { status: 500 })
}
}
+39
View File
@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const dataFile = path.join(process.cwd(), 'trading-trades.json')
export async function GET() {
try {
let trades = []
if (fs.existsSync(dataFile)) {
trades = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
}
return NextResponse.json({ trades })
} catch (error) {
return NextResponse.json({ trades: [] })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
let trades = []
if (fs.existsSync(dataFile)) {
trades = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
}
trades.push({
...body,
id: Date.now().toString(),
openedAt: new Date().toISOString()
})
fs.writeFileSync(dataFile, JSON.stringify(trades, null, 2))
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to save' }, { status: 500 })
}
}
+515
View File
@@ -0,0 +1,515 @@
"use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
type GameState = "upload" | "ready" | "playing" | "gameover";
type BossState = "idle" | "attacking" | "stunned" | "dizzy";
const powerUps: Record<string, { name: string; icon: string; duration: number }> = {
double: { name: "2x Damage", icon: "⚔️", duration: 5000 },
freeze: { name: "Stun Boss", icon: "❄️", duration: 3000 },
bomb: { name: "Bomb", icon: "💣", duration: 0 },
none: { name: "", icon: "", duration: 0 },
};
const playSound = (type: "punch" | "combo" | "powerup" | "ko" | "gameover" | "bossAttack" | "hit") => {
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
switch (type) {
case "punch":
oscillator.frequency.setValueAtTime(150, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.1);
break;
case "hit":
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.15);
gainNode.gain.setValueAtTime(0.4, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.15);
break;
case "bossAttack":
oscillator.frequency.setValueAtTime(80, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(120, audioCtx.currentTime + 0.1);
oscillator.frequency.setValueAtTime(80, audioCtx.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "combo":
oscillator.frequency.setValueAtTime(300, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(400, audioCtx.currentTime + 0.1);
oscillator.frequency.setValueAtTime(500, audioCtx.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "powerup":
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.3);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "ko":
oscillator.frequency.setValueAtTime(100, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(30, audioCtx.currentTime + 0.5);
gainNode.gain.setValueAtTime(0.8, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.5);
break;
case "gameover":
oscillator.frequency.setValueAtTime(400, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(300, audioCtx.currentTime + 0.2);
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime + 0.4);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.6);
break;
}
} catch (e) {}
};
export default function BossPunch() {
const [bossImage, setBossImage] = useState<string | null>(null);
const [gameState, setGameState] = useState<GameState>("upload");
const [score, setScore] = useState(0);
const [bossHealth, setBossHealth] = useState(100);
const [playerHealth, setPlayerHealth] = useState(100);
const [combo, setCombo] = useState(0);
const [maxCombo, setMaxCombo] = useState(0);
const [lastHit, setLastHit] = useState<"left" | "right" | null>(null);
const [playerX, setPlayerX] = useState(0);
const [bossX, setBossX] = useState(0);
const [bossRotation, setBossRotation] = useState(0);
const [bossState, setBossState] = useState<BossState>("idle");
const [isPaid, setIsPaid] = useState(false);
const [showPayModal, setShowPayModal] = useState(false);
const [activePowerUp, setActivePowerUp] = useState<string>("none");
const [powerUpCooldown, setPowerUpCooldown] = useState(0);
const [floatingTexts, setFloatingTexts] = useState<{ id: number; text: string; x: number; y: number; type: string }[]>([]);
const [screenShake, setScreenShake] = useState(false);
const [particles, setParticles] = useState<{ id: number; x: number; y: number; vx: number; vy: number; color: string; life: number }[]>([]);
const [winQuote, setWinQuote] = useState("I am sorry Haitham!");
const [showWinQuoteInput, setShowWinQuoteInput] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const floatingIdRef = useRef(0);
const particleIdRef = useRef(0);
const attackIntervalRef = useRef<NodeJS.Timeout | null>(null);
const addFloatingText = (text: string, x: number, y: number, type: string = "damage") => {
const id = floatingIdRef.current++;
setFloatingTexts(prev => [...prev, { id, text, x, y, type }]);
setTimeout(() => {
setFloatingTexts(prev => prev.filter(t => t.id !== id));
}, 1500);
};
const addParticle = (x: number, y: number, color: string) => {
const id = particleIdRef.current++;
setParticles(prev => [...prev, {
id, x, y,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10 - 5,
color, life: 1
}]);
};
useEffect(() => {
if (particles.length > 0) {
const timer = setInterval(() => {
setParticles(prev => prev.map(p => ({
...p,
x: p.x + p.vx,
y: p.y + p.vy,
vy: p.vy + 0.5,
life: p.life - 0.05
})).filter(p => p.life > 0));
}, 16);
return () => clearInterval(timer);
}
}, [particles.length]);
useEffect(() => {
if (gameState === "playing" && bossState !== "stunned" && bossState !== "dizzy") {
attackIntervalRef.current = setInterval(() => {
if (bossState === "idle" && Math.random() < 0.3) {
bossAttack();
}
}, 2000);
}
return () => {
if (attackIntervalRef.current) clearInterval(attackIntervalRef.current);
};
}, [gameState, bossState]);
const bossAttack = () => {
if (bossState !== "idle") return;
setBossState("attacking");
playSound("bossAttack");
setBossX(-30);
setTimeout(() => {
setBossX(30);
setTimeout(() => {
const damage = 5 + Math.floor(Math.random() * 10);
setPlayerHealth(prev => Math.max(0, prev - damage));
playSound("hit");
setScreenShake(true);
setTimeout(() => setScreenShake(false), 200);
addFloatingText(`-${damage}`, 30, 60, "boss");
setBossX(0);
setBossState("idle");
if (playerHealth - damage <= 0) {
setGameState("gameover");
playSound("gameover");
}
}, 150);
}, 150);
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
setBossImage(event.target?.result as string);
setGameState("ready");
};
reader.readAsDataURL(file);
}
};
const startGame = () => {
setGameState("playing");
setScore(0);
setBossHealth(100);
setPlayerHealth(100);
setCombo(0);
setMaxCombo(0);
setActivePowerUp("none");
setBossState("idle");
};
const activatePowerUp = (type: string) => {
if (powerUpCooldown > 0 || gameState !== "playing") return;
playSound("powerup");
setActivePowerUp(type);
if (type === "bomb") {
setBossHealth(prev => Math.max(0, prev - 30));
addFloatingText("-30! 💣", 70, 30, "powerup");
setScreenShake(true);
setTimeout(() => setScreenShake(false), 300);
} else if (type === "freeze") {
setBossState("stunned");
setTimeout(() => setBossState("idle"), 3000);
} else {
setTimeout(() => setActivePowerUp("none"), powerUps[type]?.duration || 5000);
}
setPowerUpCooldown(10);
};
useEffect(() => {
if (powerUpCooldown > 0) {
const timer = setInterval(() => {
setPowerUpCooldown(prev => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(timer);
}
}, [powerUpCooldown]);
const punch = useCallback((hand: "left" | "right") => {
if (gameState !== "playing" || bossState === "stunned") return;
if (Math.random() < 0.15 && bossState === "idle") {
setBossState("dizzy");
addFloatingText("Miss!", 70, 40, "miss");
setTimeout(() => setBossState("idle"), 500);
return;
}
setLastHit(hand);
setCombo(c => {
const newCombo = c + 1;
if (newCombo > maxCombo) setMaxCombo(newCombo);
if (newCombo % 10 === 0) {
playSound("combo");
addFloatingText(`${newCombo} COMBO! 🔥`, 50, 50, "combo");
}
return newCombo;
});
let damage = 5 + Math.floor(combo / 5);
damage = Math.min(damage, 20);
if (activePowerUp === "double") damage *= 2;
playSound("punch");
const newHealth = Math.max(0, bossHealth - damage);
setBossHealth(newHealth);
setScore(s => s + damage * 10);
setBossRotation(hand === "left" ? -20 - (damage * 2) : 20 + (damage * 2));
setBossX(hand === "left" ? -20 : 20);
setTimeout(() => { setBossRotation(0); setBossX(0); }, 150);
setPlayerX(hand === "left" ? 40 : -40);
setTimeout(() => setPlayerX(0), 100);
for (let i = 0; i < 5; i++) {
addParticle(70, 40, damage > 15 ? "#ff0000" : "#ffffff");
}
addFloatingText(`-${damage}`, 60 + (Math.random() * 20 - 10), 30 + (Math.random() * 20 - 10), "damage");
if (damage > 10) {
setScreenShake(true);
setTimeout(() => setScreenShake(false), 100);
}
setTimeout(() => setLastHit(null), 150);
if (newHealth <= 0) {
playSound("ko");
setGameState("gameover");
}
}, [gameState, combo, bossHealth, activePowerUp, maxCombo, bossState]);
const resetGame = () => {
setGameState("ready");
setScore(0);
setBossHealth(100);
setPlayerHealth(100);
setCombo(0);
setMaxCombo(0);
setActivePowerUp("none");
setBossState("idle");
};
const shakeClass = screenShake ? "animate-pulse" : "";
return (
<div className={`min-h-screen bg-gradient-to-b from-red-900 via-red-800 to-orange-900 flex flex-col items-center justify-center p-2 overflow-hidden ${shakeClass}`}>
{particles.map(p => (
<motion.div
key={p.id}
initial={{ x: `${p.x}%`, y: `${p.y}%`, opacity: 1 }}
animate={{ x: `${p.x + p.vx}%`, y: `${p.y + p.vy}%`, opacity: p.life }}
className="absolute w-3 h-3 rounded-full pointer-events-none"
style={{ backgroundColor: p.color }}
/>
))}
{floatingTexts.map(ft => (
<motion.div
key={ft.id}
initial={{ opacity: 1, y: 0, scale: 0.5 }}
animate={{ opacity: 0, y: -50, scale: ft.type === "boss" ? 1.5 : 1.2 }}
transition={{ duration: 1 }}
className={`absolute pointer-events-none text-2xl font-black ${ft.type === "boss" ? "text-red-400" : ft.type === "miss" ? "text-gray-400" : ft.type === "combo" ? "text-yellow-400" : "text-white"}`}
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}
>
{ft.text}
</motion.div>
))}
<div className="absolute top-0 left-0 right-0 p-2 flex justify-between items-center z-10">
<h1 className="text-xl font-black text-white tracking-wider">
BOSS<span className="text-red-500">PUNCH</span>
</h1>
{gameState === "playing" && (
<div className="bg-black/50 rounded-full px-3 py-1 flex gap-3 text-xs">
<span className="text-white font-bold">Score: {score}</span>
<span className="text-yellow-400 font-bold">Combo: {combo}x</span>
</div>
)}
</div>
{gameState === "playing" && (
<div className="absolute top-12 left-2 right-2 flex flex-col gap-1 z-10">
<div className="flex items-center gap-2">
<span className="text-xs text-white w-12">BOSS</span>
<div className="flex-1 h-3 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-red-500 to-green-500 transition-all duration-200" style={{ width: `${bossHealth}%` }} />
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-white w-12">YOU</span>
<div className="flex-1 h-3 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-green-500 to-blue-500 transition-all duration-200" style={{ width: `${playerHealth}%` }} />
</div>
</div>
</div>
)}
{gameState === "playing" && (
<div className="absolute top-24 left-0 right-0 flex justify-center gap-2 z-10">
{(["double", "freeze", "bomb"] as string[]).map(pu => (
<button
key={pu}
onClick={() => activatePowerUp(pu)}
disabled={powerUpCooldown > 0 || bossState === "stunned"}
className={`bg-white/20 backdrop-blur rounded-full p-2 text-xl transition-all hover:scale-110 disabled:opacity-50 disabled:cursor-not-allowed ${activePowerUp === pu ? "ring-2 ring-yellow-400" : ""}`}
title={powerUps[pu]?.name}
>
{powerUps[pu]?.icon}
</button>
))}
{powerUpCooldown > 0 && <span className="absolute -bottom-2 text-xs text-white/70">{powerUpCooldown}s</span>}
</div>
)}
<div className="relative w-full max-w-lg aspect-square max-h-[45vh] mt-8">
<AnimatePresence>
{bossImage && (
<motion.div
className="absolute right-0 top-1/3"
animate={{
x: bossX,
rotate: bossState === "dizzy" ? [0, -10, 10, -10, 10, 0] : bossRotation,
scale: bossState === "stunned" ? 0.9 : bossHealth > 0 ? 1 : 0.8,
opacity: bossState === "stunned" ? 0.7 : 1
}}
transition={{ type: "spring", stiffness: 300 }}
>
<div className="relative w-32 h-32 md:w-48 md:h-48">
<div className={`absolute -left-6 top-1/2 w-10 h-10 ${activePowerUp === "freeze" ? "bg-cyan-300" : bossState === "dizzy" ? "bg-gray-400" : "bg-red-600"} rounded-full border-4 border-red-400 flex items-center justify-center`}>
<span className="text-white font-bold text-xs">L</span>
</div>
<div className={`absolute -right-6 top-1/3 w-10 h-10 ${activePowerUp === "freeze" ? "bg-cyan-300" : bossState === "dizzy" ? "bg-gray-400" : "bg-red-600"} rounded-full border-4 border-red-400 flex items-center justify-center`}>
<span className="text-white font-bold text-xs">R</span>
</div>
<div className={`w-full h-full rounded-2xl overflow-hidden border-4 ${activePowerUp === "freeze" ? "border-cyan-400" : bossState === "stunned" ? "border-blue-400" : "border-white"} shadow-2xl`}>
<img src={bossImage} alt="Boss" className="w-full h-full object-cover" />
</div>
{bossState === "stunned" && <div className="absolute -top-4 left-1/2 -translate-x-1/2 text-2xl">💫</div>}
{bossState === "dizzy" && <div className="absolute top-0 right-0 text-xl">😵</div>}
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{gameState === "playing" && (
<motion.div className="absolute left-0 bottom-0" animate={{ x: playerX }} transition={{ duration: 0.1 }}>
<div className="w-20 h-28 md:w-28 md:h-36 relative">
<motion.div className="absolute top-1/3 -right-3 w-12 h-12 bg-blue-600 border-4 border-blue-400 rounded-full cursor-pointer flex items-center justify-center text-2xl" whileTap={{ scale: 0.8 }} onClick={() => punch("right")}>👊</motion.div>
<motion.div className="absolute top-1/3 -left-3 w-12 h-12 bg-blue-600 border-4 border-blue-400 rounded-full cursor-pointer flex items-center justify-center text-2xl" whileTap={{ scale: 0.8 }} onClick={() => punch("left")}>👊</motion.div>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-20 bg-blue-700 rounded-t-xl" />
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-amber-200 rounded-full" />
</div>
</motion.div>
)}
</AnimatePresence>
{lastHit && (
<motion.div initial={{ opacity: 1, scale: 0.5 }} animate={{ opacity: 0, scale: 1.5 }} className="absolute right-1/4 top-1/3 text-5xl font-black text-yellow-400">
Pow!
</motion.div>
)}
</div>
<div className="mt-2 w-full max-w-md">
{gameState === "upload" && (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center">
<div className="bg-white/10 backdrop-blur rounded-2xl p-6 border-2 border-dashed border-white/30">
<div className="text-5xl mb-3">👔</div>
<h2 className="text-xl font-bold text-white mb-2">Upload Your Boss!</h2>
<p className="text-white/70 mb-4">Take out your frustrations!</p>
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleImageUpload} className="hidden" />
<button onClick={() => fileInputRef.current?.click()} className="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-8 rounded-full text-lg transition-all hover:scale-105 shadow-lg">
📸 Upload Photo
</button>
<p className="text-xs text-white/50 mt-4">Free with ads $9.99 for ad-free</p>
</div>
</motion.div>
)}
{gameState === "ready" && bossImage && (
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="text-center">
<div className="bg-white/10 backdrop-blur rounded-xl p-3 mb-3 border border-white/20">
<button onClick={() => setShowWinQuoteInput(!showWinQuoteInput)} className="text-white/70 text-sm hover:text-white flex items-center justify-center gap-2">
🏆 {showWinQuoteInput ? "Hide" : "If u win he says..."}
</button>
{showWinQuoteInput && (
<input
type="text"
value={winQuote}
onChange={(e) => setWinQuote(e.target.value)}
placeholder="What does boss say when u win?"
className="w-full mt-2 bg-white/20 border border-white/30 rounded-lg px-3 py-2 text-white placeholder-white/50 text-sm"
/>
)}
</div>
<h2 className="text-xl font-bold text-white mb-2">Ready to Fight!</h2>
<p className="text-white/70 mb-4">Boss fights back! Don't lose!</p>
<button onClick={startGame} className="bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white font-bold py-3 px-10 rounded-full text-lg transition-all hover:scale-105 shadow-xl">
🥊 START FIGHT
</button>
</motion.div>
)}
{gameState === "playing" && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<p className="text-white/70 text-sm mb-2">Tap to punch!</p>
<div className="flex gap-4 justify-center">
<button onClick={() => punch("left")} className="bg-blue-600 hover:scale-110 w-20 h-20 rounded-full border-4 border-blue-400 flex items-center justify-center text-3xl active:scale-90 transition-transform">👊</button>
<button onClick={() => punch("right")} className="bg-blue-600 hover:scale-110 w-20 h-20 rounded-full border-4 border-blue-400 flex items-center justify-center text-3xl active:scale-90 transition-transform">👊</button>
</div>
</motion.div>
)}
{gameState === "gameover" && (
<motion.div initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} className="text-center">
<div className={`rounded-2xl p-6 border-4 ${bossHealth <= 0 ? "bg-gradient-to-b from-yellow-500 to-orange-500 border-yellow-300" : "bg-gradient-to-b from-red-600 to-red-800 border-red-400"}`}>
<div className="text-6xl mb-2">{bossHealth <= 0 ? "🏆" : "💀"}</div>
<h2 className="text-3xl font-black text-white mb-2">{bossHealth <= 0 ? "KNOCKOUT!" : "YOU LOSE!"}</h2>
{bossHealth <= 0 && (
<div className="bg-white/20 rounded-lg p-3 mb-3">
<p className="text-lg font-bold text-white">Boss says:</p>
<p className="text-white text-xl">"{winQuote}"</p>
</div>
)}
<p className="text-xl font-bold text-white mb-2">Score: {score}</p>
<p className="text-white/80 mb-4">Max Combo: {maxCombo}x</p>
<div className="space-y-2">
<button onClick={resetGame} className="w-full bg-white text-red-600 font-bold py-2 px-6 rounded-full text-base transition-all hover:scale-105">
🔄 Fight Again
</button>
{!isPaid && (
<button onClick={() => setShowPayModal(true)} className="w-full bg-yellow-400 text-yellow-900 font-bold py-2 px-6 rounded-full text-base transition-all hover:scale-105 flex items-center justify-center gap-2">
$9.99 - No Ads
</button>
)}
</div>
</div>
</motion.div>
)}
</div>
{/* Stripe Payment Modal */}
{showPayModal && (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full text-center">
<h3 className="text-2xl font-bold text-gray-800 mb-2">Remove Ads</h3>
<p className="text-gray-600 mb-4">Get BossPunch ad-free forever!</p>
<p className="text-4xl font-black text-purple-600 mb-4">$9.99</p>
<button className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 rounded-lg mb-2 transition">
Pay with Card
</button>
<button onClick={() => setShowPayModal(false)} className="text-gray-500 text-sm">Cancel</button>
</div>
</div>
)}
{!isPaid && gameState !== "upload" && (
<div className="fixed bottom-0 left-0 right-0 bg-black/80 py-2 text-center">
<p className="text-white/60 text-xs">Advertisement</p>
</div>
)}
</div>
);
}
+315
View File
@@ -0,0 +1,315 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
type Booking = {
id: string;
customer: string;
service: string;
date: string;
time: string;
status: "confirmed" | "pending" | "cancelled";
};
type Lead = {
id: string;
name: string;
phone: string;
message: string;
date: string;
status: "new" | "contacted" | "qualified";
};
// Mock data for demo
const mockBookings: Booking[] = [
{ id: "1", customer: "María García", service: "Cita consulta", date: "2026-02-17", time: "10:00", status: "confirmed" },
{ id: "2", customer: "John Smith", service: "Treatment", date: "2026-02-17", time: "14:30", status: "pending" },
{ id: "3", customer: "Carlos Ruiz", service: "Reserva mesa", date: "2026-02-18", time: "20:00", status: "confirmed" },
{ id: "4", customer: "Ana López", service: "Cita estética", date: "2026-02-19", time: "11:00", status: "confirmed" },
{ id: "5", customer: "Pierre Dubois", service: "Booking", date: "2026-02-19", time: "16:00", status: "confirmed" },
];
const mockLeads: Lead[] = [
{ id: "1", name: "María García", phone: "+34 612 345 678", message: "Hola, me gustaría información sobre...", date: "2026-02-16", status: "new" },
{ id: "2", name: "John Smith", phone: "+44 7700 900123", message: "Do you have availability for next week?", date: "2026-02-16", status: "qualified" },
{ id: "3", name: "Carlos Ruiz", phone: "+34 654 987 321", message: "Quiero reservar para 4 personas", date: "2026-02-15", status: "contacted" },
];
export default function DashboardPage() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loginError, setLoginError] = useState("");
const [activeTab, setActiveTab] = useState<"bookings" | "leads" | "analytics">("bookings");
const handleLogin = () => {
if (email === "User" && password === "#SiteMenteUserpass2026") {
setIsLoggedIn(true);
setLoginError("");
} else {
setLoginError("Invalid credentials");
}
};
if (!isLoggedIn) {
return (
<div className="min-h-screen bg-[#1a1625] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md"
>
<div className="text-center mb-8">
<div className="text-5xl mb-4">🏢</div>
<h1 className="text-3xl font-bold text-white mb-2">SiteMente</h1>
<p className="text-white/60">Client Dashboard</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
<div className="space-y-4">
<div>
<label className="text-sm text-white/60 mb-1 block">Username</label>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="User"
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder:text-white/30"
/>
</div>
<div>
<label className="text-sm text-white/60 mb-1 block">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder:text-white/30"
/>
</div>
{loginError && (
<p className="text-red-400 text-sm">{loginError}</p>
)}
<button
onClick={handleLogin}
className="w-full py-3 bg-brand-pink rounded-lg font-semibold text-white hover:bg-[#ff7bc0] transition"
>
Login
</button>
<p className="text-center text-white/40 text-sm">
Ask SiteMente to create your dashboard
</p>
</div>
</div>
</motion.div>
</div>
);
}
const stats = {
totalBookings: mockBookings.length,
confirmed: mockBookings.filter(b => b.status === "confirmed").length,
pending: mockBookings.filter(b => b.status === "pending").length,
totalLeads: mockLeads.length,
newLeads: mockLeads.filter(l => l.status === "new").length,
};
return (
<div className="min-h-screen bg-[#1a1625] text-white">
{/* Header */}
<header className="border-b border-white/10 bg-[#1a1625]/90 backdrop-blur sticky top-0 z-50">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<span className="text-2xl">🏢</span>
<span className="font-bold text-xl">SiteMente Dashboard</span>
</div>
<div className="flex items-center gap-4">
<span className="text-white/60">SiteMente Restaurant Demo</span>
<button
onClick={() => setIsLoggedIn(false)}
className="text-sm text-white/60 hover:text-white"
>
Logout
</button>
</div>
</div>
</header>
<main className="mx-auto max-w-6xl px-6 py-8">
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-2xl font-bold text-brand-pink">{stats.totalBookings}</p>
<p className="text-sm text-white/60">Total Bookings</p>
</div>
<div className="bg-green-500/10 rounded-xl p-4 border border-green-500/30">
<p className="text-2xl font-bold text-green-400">{stats.confirmed}</p>
<p className="text-sm text-white/60">Confirmed</p>
</div>
<div className="bg-yellow-500/10 rounded-xl p-4 border border-yellow-500/30">
<p className="text-2xl font-bold text-yellow-400">{stats.pending}</p>
<p className="text-sm text-white/60">Pending</p>
</div>
<div className="bg-blue-500/10 rounded-xl p-4 border border-blue-500/30">
<p className="text-2xl font-bold text-blue-400">{stats.totalLeads}</p>
<p className="text-sm text-white/60">Total Leads</p>
</div>
<div className="bg-purple-500/10 rounded-xl p-4 border border-purple-500/30">
<p className="text-2xl font-bold text-purple-400">{stats.newLeads}</p>
<p className="text-sm text-white/60">New Leads</p>
</div>
</div>
{/* Tabs */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setActiveTab("bookings")}
className={`px-4 py-2 rounded-lg font-medium transition ${activeTab === "bookings" ? "bg-brand-pink" : "bg-white/10 hover:bg-white/20"}`}
>
📅 Bookings
</button>
<button
onClick={() => setActiveTab("leads")}
className={`px-4 py-2 rounded-lg font-medium transition ${activeTab === "leads" ? "bg-brand-pink" : "bg-white/10 hover:bg-white/20"}`}
>
📩 Leads
</button>
<button
onClick={() => setActiveTab("analytics")}
className={`px-4 py-2 rounded-lg font-medium transition ${activeTab === "analytics" ? "bg-brand-pink" : "bg-white/10 hover:bg-white/20"}`}
>
📈 Analytics
</button>
</div>
{/* Bookings Tab */}
{activeTab === "bookings" && (
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<table className="w-full">
<thead className="bg-white/5 border-b border-white/10">
<tr>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Customer</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Service</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Date</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Time</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Status</th>
</tr>
</thead>
<tbody>
{mockBookings.map((booking) => (
<tr key={booking.id} className="border-b border-white/5">
<td className="px-4 py-3">{booking.customer}</td>
<td className="px-4 py-3">{booking.service}</td>
<td className="px-4 py-3">{booking.date}</td>
<td className="px-4 py-3">{booking.time}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs ${
booking.status === "confirmed" ? "bg-green-500/20 text-green-400" :
booking.status === "pending" ? "bg-yellow-500/20 text-yellow-400" :
"bg-red-500/20 text-red-400"
}`}>
{booking.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Leads Tab */}
{activeTab === "leads" && (
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<table className="w-full">
<thead className="bg-white/5 border-b border-white/10">
<tr>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Name</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Phone</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Message</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Date</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Status</th>
</tr>
</thead>
<tbody>
{mockLeads.map((lead) => (
<tr key={lead.id} className="border-b border-white/5">
<td className="px-4 py-3">{lead.name}</td>
<td className="px-4 py-3">
<a href={`tel:${lead.phone}`} className="text-brand-pink hover:underline">{lead.phone}</a>
</td>
<td className="px-4 py-3 text-white/70 text-sm">{lead.message}</td>
<td className="px-4 py-3">{lead.date}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs ${
lead.status === "new" ? "bg-blue-500/20 text-blue-400" :
lead.status === "qualified" ? "bg-green-500/20 text-green-400" :
"bg-yellow-500/20 text-yellow-400"
}`}>
{lead.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Analytics Tab */}
{activeTab === "analytics" && (
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-white/5 rounded-xl border border-white/10 p-6">
<h3 className="text-lg font-semibold mb-4">Bookings This Week</h3>
<div className="flex items-end gap-2 h-40">
{[3, 5, 2, 4, 6, 3, 5].map((h, i) => (
<div key={i} className="flex-1 bg-brand-pink/50 rounded-t" style={{ height: `${h * 14}%` }}></div>
))}
</div>
<div className="flex justify-between mt-2 text-xs text-white/40">
<span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span><span>Sun</span>
</div>
</div>
<div className="bg-white/5 rounded-xl border border-white/10 p-6">
<h3 className="text-lg font-semibold mb-4">Lead Sources</h3>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>🌐 Website</span>
<span className="text-brand-pink">65%</span>
</div>
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-brand-pink w-[65%]"></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>💬 WhatsApp</span>
<span className="text-green-400">25%</span>
</div>
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-green-500 w-[25%]"></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>📞 Phone</span>
<span className="text-blue-400">10%</span>
</div>
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-blue-500 w-[10%]"></div>
</div>
</div>
</div>
</div>
</div>
)}
<p className="text-center text-white/40 text-sm mt-8">
Powered by SiteMente AI Your AI Employee working 24/7
</p>
</main>
</div>
);
}
+32
View File
@@ -0,0 +1,32 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Demos | SiteMente - Webs IA para cada industria",
description:
"Ver demos de webs con IA para inmobiliarias, restaurantes, clínicas y servicios del hogar. Precios claros desde €390/mes.",
keywords: [
"demo web IA España",
"inteligencia artificial inmobiliarias",
"AI restaurantesdemo",
"chatbot clínica",
"IA servicios hogar",
"web con AI Costa del Sol",
],
openGraph: {
title: "Demos SiteMente | Webs IA por industria",
description:
"Ver demos de webs con IA para cada tipo de negocio. Desde €390/mes.",
url: "https://sitemente.com/demos",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
},
],
},
};
export default function DemosLayout({ children }: { children: React.ReactNode }) {
return children;
}
+2 -1054
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -100,3 +100,11 @@ body {
opacity: 0;
}
}
@keyframes gradient {
0% { background-position: 0% center; }
100% { background-position: 200% center; }
}
.animate-gradient {
animation: gradient 3s linear infinite;
}
+53
View File
@@ -0,0 +1,53 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "HolaCompi - AI Voice Agents for Spanish Businesses",
description: "Building the future of automated customer calls",
};
export default function HolaCompiPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-[#1a1625] to-[#2d1f3d] text-white flex items-center justify-center overflow-hidden">
{/* Background effects */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-20 left-10 text-6xl animate-pulse"></div>
<div className="absolute top-40 right-20 text-5xl animate-bounce" style={{ animationDuration: '2s' }}>🚀</div>
<div className="absolute bottom-40 left-20 text-5xl animate-pulse" style={{ animationDelay: '0.5s' }}>💫</div>
<div className="absolute bottom-20 right-10 text-6xl animate-bounce" style={{ animationDuration: '3s' }}>🤖</div>
</div>
<div className="text-center px-6 relative z-10">
<div className="text-7xl mb-8 animate-bounce" style={{ animationDuration: '2s' }}>
🤖📞
</div>
<h1 className="text-6xl font-bold mb-6 bg-gradient-to-r from-pink-500 via-purple-500 to-pink-500 bg-[length:200%_auto] animate-gradient bg-clip-text text-transparent">
HolaCompi
</h1>
<h2 className="text-3xl text-white/90 mb-6 font-light">
AI Voice Agents for Spanish Businesses
</h2>
<div className="inline-block bg-gradient-to-r from-pink-500/30 to-purple-500/30 border border-pink-500/40 rounded-full px-8 py-3 mb-10 backdrop-blur">
<span className="text-2xl font-medium">🚀 Launching March 2026</span>
</div>
<p className="text-xl text-white/70 mb-10 max-w-md mx-auto">
Building the future of automated customer calls
</p>
<div className="text-white/50 text-lg">
<p>For inquiries: <a href="mailto:Holac@HolaCompi.com" className="text-pink-400 hover:text-pink-300 transition">Holac@HolaCompi.com</a></p>
</div>
<div className="mt-16 pt-8 border-t border-white/10">
<Link href="/" className="text-pink-400 hover:text-pink-300 transition text-lg">
Back to SiteMente
</Link>
</div>
</div>
</div>
);
}
+337
View File
@@ -0,0 +1,337 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { motion } from "framer-motion";
type LeadStatus = "new" | "contacted" | "qualified" | "proposal" | "won" | "lost";
interface Lead {
id: string;
name: string;
category: "restaurant" | "real-estate" | "clinic" | "car-rental" | "hp-client" | "other";
phone: string;
email: string;
website: string;
address: string;
rating: number;
score: number;
notes: string;
status: LeadStatus;
lastContact: string;
nextAction: string;
createdAt: string;
}
const allLeads: Lead[] = [
// New leads from research Feb 17
{ id: "r11", name: "Restaurante Milan", category: "restaurant", phone: "+34 952 44 58 55", email: "", website: "", address: "Av. Federico Garcia Lorca 7, 29630 Benalmádena", rating: 0, score: 8, notes: "Italian restaurant - call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r12", name: "Tex Mex Gringos", category: "restaurant", phone: "", email: "reservas@restaurantespuertomarina.com", website: "restaurantespuertomarina.com", address: "Puerto Marina, Benalmádena", rating: 0, score: 8, notes: "Email found - send AI proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "r13", name: "Lime & Lemon Tapas", category: "restaurant", phone: "", email: "info@limeandlemonbenalmadena.com", website: "limeandlemonbenalmadena.com", address: "Av. Las Palmeras 1, 29630 Benalmádena", rating: 0, score: 8, notes: "Email found - send AI proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "r14", name: "El Parador", category: "restaurant", phone: "+34 952 44 92 93", email: "", website: "", address: "Av. Juan Luis Peralta 47, 29639 Benalmádena", rating: 4.0, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r15", name: "Escorpio Restaurante", category: "restaurant", phone: "+34 952 569 047", email: "", website: "", address: "Santo Domingo de Guzmán 7, Benalmádena", rating: 0, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r16", name: "The Bull Bar", category: "restaurant", phone: "+34 646 569 374", email: "", website: "", address: "Av del Chorrillo 15, Benalmádena", rating: 0, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r17", name: "La Plaza Restaurant", category: "restaurant", phone: "+34 952 44 84 83", email: "", website: "", address: "Plaza de Espana 2, 29639 Benalmádena", rating: 4.3, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r18", name: "Caliu Restaurant", category: "restaurant", phone: "", email: "caliu.torremolinos@gmail.com", website: "", address: "Torremolinos", rating: 0, score: 7, notes: "Email found - send proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "r19", name: "The Carvery", category: "restaurant", phone: "", email: "info@thecarverycompany.com", website: "", address: "Benalmádena", rating: 0, score: 7, notes: "Email found - send proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "re6", name: "Engel & Völkers Costa del Sol", category: "real-estate", phone: "+34 952 650 234", email: "", website: "", address: "CC Diana Local 23, 29688 Estepona", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re7", name: "Your Viva Marbella", category: "real-estate", phone: "+34 951 27 27 43", email: "", website: "", address: "CC El Rosario, 29604 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re8", name: "Panorama Properties", category: "real-estate", phone: "+34 952 774 266", email: "", website: "", address: "Hotel Local 23, 29602 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re9", name: "Marbella For Sale", category: "real-estate", phone: "+34 952 907 386", email: "", website: "", address: "Edif. Marina Banús, 29660 Puerto Banús", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re10", name: "Domus Venari", category: "real-estate", phone: "+34 952 444 295", email: "", website: "", address: "Ctra. N340 KM189, 29604 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re11", name: "Diana Morales Properties", category: "real-estate", phone: "+34 952 765 138", email: "", website: "", address: "Av. Cánovas del Castillo 4, 29601 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re12", name: "Hacienda Estates", category: "real-estate", phone: "+34 952 850 154", email: "", website: "", address: "CC Pinogolf Local 2, 29604 Elviria", rating: 0, score: 7, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re13", name: "Sun Med Estates", category: "real-estate", phone: "+34 952 493 372", email: "", website: "", address: "c/ Sedella 3, La Cala de Mijas", rating: 0, score: 7, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "c4", name: "Smart Dental (new)", category: "clinic", phone: "+34 911 98 04 65", email: "", website: "", address: "Av. Blas Infante 17, 29631 Benalmádena", rating: 0, score: 9, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "c5", name: "Grupo Dental Clinics", category: "clinic", phone: "", email: "", website: "", address: "Av. Cdad. de Melilla 26, 29639 Benalmádena", rating: 0, score: 7, notes: "Find contact info", status: "new", lastContact: "", nextAction: "Find phone", createdAt: "2026-02-17" },
{ id: "cr3", name: "Marbesol Car Rental", category: "car-rental", phone: "+34 952 93 44 12", email: "", website: "", address: "Málaga Airport", rating: 0, score: 7, notes: "Offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
// Original leads
{ id: "r1", name: "Tex Mex Gringos", category: "restaurant", phone: "+34 951 777 848", email: "info@restaurantespuertomarina.com", website: "restaurantespuertomarina.com", address: "Calle La Fragata, s/n, 29630 Benalmádena", rating: 4.5, score: 7, notes: "Has website - offer AI employee 24/7", status: "new", lastContact: "", nextAction: "Call - offer AI employee", createdAt: "2026-02-16" },
{ id: "r2", name: "La Mar Chica", category: "restaurant", phone: "+34 951 634 708", email: "info.lamarchica@gmail.com", website: "mar-chica.com", address: "Calle Marbella, 1, 29639 Benalmádena Pueblo", rating: 4.3, score: 7, notes: "Has website - offer AI employee 24/7", status: "new", lastContact: "", nextAction: "Call - offer AI employee", createdAt: "2026-02-16" },
{ id: "r3", name: "SALU Grill & Wine", category: "restaurant", phone: "+34 951 715 736", email: "salu.spain@gmail.com", website: "salu-restaurant.com", address: "Calle San José, 6, Benalmádena Pueblo", rating: 4.6, score: 8, notes: "Good site - offer AI employee upgrade", status: "new", lastContact: "", nextAction: "Schedule demo", createdAt: "2026-02-16" },
{ id: "r4", name: "Basil", category: "restaurant", phone: "631 971 592", email: "", website: "basilbenalmadena.com", address: "Plaza Nueva Bonanza, 29630 Benalmádena", rating: 4.4, score: 6, notes: "Has WhatsApp - offer AI employee", status: "new", lastContact: "", nextAction: "Call - offer AI employee", createdAt: "2026-02-16" },
{ id: "r5", name: "Lime & Lemon Tapas", category: "restaurant", phone: "", email: "", website: "limeandlemonbenalmadena.com", address: "Benalmádena", rating: 0, score: 5, notes: "Has website - offer AI employee 24/7", status: "new", lastContact: "", nextAction: "Find phone first", createdAt: "2026-02-16" },
{ id: "r6", name: "Restaurant No7", category: "restaurant", phone: "+34 655 036 827", email: "", website: "", address: "Benalmádena", rating: 0, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "r7", name: "Capitan Bar & Restaurant", category: "restaurant", phone: "+34 674 591 584", email: "", website: "", address: "Benalmádena", rating: 0, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "r8", name: "TORO Puerto Marina", category: "restaurant", phone: "+34 952 913 177", email: "", website: "restaurantespuertomarina.com/toro-puerto-marina", address: "Puerto Marina, Benalmádena", rating: 4.5, score: 7, notes: "Has website - offer AI employee", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "r9", name: "Trocadero Benalmádena", category: "restaurant", phone: "+34 681 142 944", email: "", website: "", address: "Avenida del Sol 121, Benalmádena", rating: 4.2, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "r10", name: "Restaurante La Nina", category: "restaurant", phone: "+34 952 449 193", email: "", website: "", address: "Plaza de Espana, Benalmádena Pueblo", rating: 4.4, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "re1", name: "Hernán Bustos Real Estate", category: "real-estate", phone: "", email: "", website: "hernanbustos.com", address: "Benalmádena, Torremolinos", rating: 0, score: 9, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Find contact info", createdAt: "2026-02-16" },
{ id: "re2", name: "ViVi Real Estate", category: "real-estate", phone: "", email: "", website: "vivi-realestate.com", address: "Costa del Sol", rating: 0, score: 9, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Find contact info", createdAt: "2026-02-16" },
{ id: "re3", name: "Marbella Mundo", category: "real-estate", phone: "", email: "", website: "marbellamundo.es", address: "Fuengirola, Costa del Sol", rating: 0, score: 9, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "re4", name: "Homenetspain", category: "real-estate", phone: "+34 633 300 956", email: "", website: "homenetspain.com", address: "Fuengirola", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Call and demo", createdAt: "2026-02-16" },
{ id: "re5", name: "Costa Listings", category: "real-estate", phone: "", email: "", website: "costalistings.es", address: "Benalmádena", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Find contact info", createdAt: "2026-02-16" },
{ id: "c1", name: "Vithas Xanit Hospital", category: "clinic", phone: "+34 952 367 190", email: "info.xanit@vithas.es", website: "vithas.es", address: "Avenida de los Argonautas, s/n, 29631 Benalmádena", rating: 4.5, score: 8, notes: "BIG opportunity - AI employee", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "c2", name: "Smart Dental", category: "clinic", phone: "", email: "", website: "", address: "Benalmádena", rating: 4.5, score: 10, notes: "NO WEBSITE - BIG opportunity", status: "new", lastContact: "", nextAction: "Find phone", createdAt: "2026-02-16" },
{ id: "c3", name: "Rosasco Dental", category: "clinic", phone: "", email: "", website: "", address: "Benalmádena", rating: 4.2, score: 10, notes: "NO WEBSITE - BIG opportunity", status: "new", lastContact: "", nextAction: "Find phone", createdAt: "2026-02-16" },
{ id: "cr1", name: "Malaga U Drive", category: "car-rental", phone: "", email: "", website: "malagaudrive.com", address: "Malaga/Benalmádena", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "cr2", name: "ALL IN Car Hire", category: "car-rental", phone: "", email: "", website: "allincarhire.com", address: "Malaga/Benalmádena", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
];
const categoryIcons: Record<string, string> = {
restaurant: "🍽️",
"real-estate": "🏠",
clinic: "🏥",
"car-rental": "🚗",
other: "📌",
};
const statusColors: Record<LeadStatus, string> = {
new: "bg-blue-500/20 text-blue-400 border-blue-500/30",
contacted: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
qualified: "bg-purple-500/20 text-purple-400 border-purple-500/30",
proposal: "bg-orange-500/20 text-orange-400 border-orange-500/30",
won: "bg-green-500/20 text-green-400 border-green-500/30",
lost: "bg-red-500/20 text-red-400 border-red-500/30",
};
type SortField = "name" | "category" | "score" | "status" | "phone";
export default function LeadsPage() {
const [leads, setLeads] = useState<Lead[]>(allLeads);
const [filter, setFilter] = useState<string>("all");
const [tab, setTab] = useState<"leads" | "email">("leads");
const [sortField, setSortField] = useState<SortField>("score");
const [sortAsc, setSortAsc] = useState(false);
const [selectedLead, setSelectedLead] = useState<Lead | null>(null);
const [search, setSearch] = useState("");
const leadsWithEmail = useMemo(() => leads.filter(l => l.email), [leads]);
useEffect(() => {
const saved = localStorage.getItem("sitemente:leads");
if (saved) {
setLeads(JSON.parse(saved));
} else {
localStorage.setItem("sitemente:leads", JSON.stringify(allLeads));
}
}, []);
const saveLeads = (newLeads: Lead[]) => {
setLeads(newLeads);
localStorage.setItem("sitemente:leads", JSON.stringify(newLeads));
};
const updateStatus = (id: string, status: LeadStatus) => {
const updated = leads.map((l) =>
l.id === id ? { ...l, status, lastContact: new Date().toISOString().split("T")[0] } : l
);
saveLeads(updated);
};
const filteredLeads = useMemo(() => {
return leads
.filter((l) => {
if (filter !== "all" && l.category !== filter && l.status !== filter) return false;
if (search && !l.name.toLowerCase().includes(search.toLowerCase())) return false;
return true;
})
.sort((a, b) => {
let cmp = 0;
if (sortField === "name") cmp = a.name.localeCompare(b.name);
else if (sortField === "category") cmp = a.category.localeCompare(b.category);
else if (sortField === "score") cmp = b.score - a.score;
else if (sortField === "status") cmp = a.status.localeCompare(b.status);
else if (sortField === "phone") cmp = (a.phone ? 1 : 0) - (b.phone ? 1 : 0);
return sortAsc ? cmp : -cmp;
});
}, [leads, filter, sortField, sortAsc, search]);
const stats = {
total: leads.length,
withPhone: leads.filter(l => l.phone).length,
noWebsite: leads.filter(l => !l.website).length,
hot: leads.filter(l => l.score >= 9).length,
new: leads.filter((l) => l.status === "new").length,
won: leads.filter((l) => l.status === "won").length,
};
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortAsc(!sortAsc);
} else {
setSortField(field);
setSortAsc(false);
}
};
const SortIcon = ({ field }: { field: SortField }) => (
<span className="ml-1 opacity-50">{sortField === field ? (sortAsc ? "↑" : "↓") : "↕"}</span>
);
return (
<div className="min-h-screen bg-[#1a1625] text-white" suppressHydrationWarning>
<header className="border-b border-white/10 bg-[#1a1625]/90 backdrop-blur sticky top-0 z-50">
<div className="mx-auto flex w-full max-w-7xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<a href="/" className="flex items-center gap-3">
<img src="/sitemente-logo-light.png" alt="SiteMente" width={40} height={40} className="h-10 w-auto" />
<span className="font-bold text-xl">Leads CRM</span>
</a>
</div>
<div className="flex items-center gap-3">
<a href="/mission-control" className="px-4 py-2 bg-white/10 rounded-lg text-sm hover:bg-white/20 transition"> Mission Control</a>
</div>
</div>
</header>
<main className="mx-auto max-w-7xl px-6 py-8" suppressHydrationWarning>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-2xl font-bold text-brand-pink">{stats.total}</p>
<p className="text-sm text-white/60">Total Leads</p>
</div>
<div className="bg-green-500/10 rounded-xl p-4 border border-green-500/30">
<p className="text-2xl font-bold text-green-400">{stats.withPhone}</p>
<p className="text-sm text-white/60">With Phone</p>
</div>
<div className="bg-red-500/10 rounded-xl p-4 border border-red-500/30">
<p className="text-2xl font-bold text-red-400">{stats.noWebsite}</p>
<p className="text-sm text-white/60">No Website</p>
</div>
<div className="bg-orange-500/10 rounded-xl p-4 border border-orange-500/30">
<p className="text-2xl font-bold text-orange-400">{stats.hot}</p>
<p className="text-sm text-white/60">Hot (9-10)</p>
</div>
<div className="bg-blue-500/10 rounded-xl p-4 border border-blue-500/30">
<p className="text-2xl font-bold text-blue-400">{stats.won}</p>
<p className="text-sm text-white/60">Won</p>
</div>
</div>
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex gap-2 bg-white/10 rounded-lg p-1">
<button onClick={() => setTab("leads")} className={`px-4 py-2 rounded-lg transition ${tab === "leads" ? "bg-brand-pink text-white" : "hover:bg-white/10"}`}>📋 Leads ({leads.length})</button>
<button onClick={() => setTab("email")} className={`px-4 py-2 rounded-lg transition ${tab === "email" ? "bg-brand-pink text-white" : "hover:bg-white/10"}`}> Email Outreach ({leadsWithEmail.length})</button>
</div>
{tab === "leads" && (
<>
<input type="text" placeholder="Search leads..." value={search} onChange={(e) => setSearch(e.target.value)} className="bg-white/10 border border-white/20 rounded-lg px-4 py-2 min-w-[200px]" />
<select value={filter} onChange={(e) => setFilter(e.target.value)} className="bg-white/10 border border-white/20 rounded-lg px-4 py-2">
<option value="all">All ({leads.length})</option>
<option value="restaurant">Restaurants ({leads.filter(l => l.category === 'restaurant').length})</option>
<option value="real-estate">Real Estate ({leads.filter(l => l.category === 'real-estate').length})</option>
<option value="clinic">Clinics ({leads.filter(l => l.category === 'clinic').length})</option>
<option value="car-rental">Car Rental ({leads.filter(l => l.category === 'car-rental').length})</option>
</select>
</>
)}
</div>
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-white/5 border-b border-white/10">
<tr>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("name")}>Lead <SortIcon field="name" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("category")}>Category <SortIcon field="category" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("phone")}>Phone <SortIcon field="phone" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("score")}>Score <SortIcon field="score" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("status")}>Status <SortIcon field="status" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Actions</th>
</tr>
</thead>
<tbody>
{filteredLeads.map((lead) => (
<motion.tr key={lead.id} initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="border-b border-white/5 hover:bg-white/5">
<td className="px-4 py-3">
<p className="font-medium">{lead.name}</p>
<p className="text-xs text-white/50 truncate max-w-[200px]">{lead.address}</p>
</td>
<td className="px-4 py-3"><span className="text-lg">{categoryIcons[lead.category]}</span></td>
<td className="px-4 py-3">
{lead.phone ? <a href={"tel:" + lead.phone} className="text-green-400 hover:underline">{lead.phone}</a> : <span className="text-red-400 text-xs">No phone</span>}
</td>
<td className="px-4 py-3">
<span className={"inline-flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold " + (lead.score >= 9 ? "bg-green-500/20 text-green-400" : lead.score >= 7 ? "bg-yellow-500/20 text-yellow-400" : "bg-red-500/20 text-red-400")}>{lead.score}</span>
</td>
<td className="px-4 py-3">
<select value={lead.status} onChange={(e) => updateStatus(lead.id, e.target.value as LeadStatus)} className={"text-xs px-2 py-1 rounded-full border " + statusColors[lead.status as LeadStatus]}>
<option value="new">New</option>
<option value="contacted">Contacted</option>
<option value="qualified">Qualified</option>
<option value="proposal">Proposal</option>
<option value="won">Won</option>
<option value="lost">Lost</option>
</select>
</td>
<td className="px-4 py-3">
<div className="flex gap-1">
{lead.phone && <a href={"tel:" + lead.phone} className="p-2 bg-green-500/20 rounded-lg hover:bg-green-500/30 transition" title="Call">📞</a>}
{lead.email && <a href={"mailto:" + lead.email} className="p-2 bg-blue-500/20 rounded-lg hover:bg-blue-500/30 transition" title="Email"></a>}
{lead.website && <a href={lead.website.startsWith("http") ? lead.website : "https://" + lead.website} target="_blank" rel="noopener noreferrer" className="p-2 bg-purple-500/20 rounded-lg hover:bg-purple-500/30 transition" title="Website">🌐</a>}
<button onClick={() => setSelectedLead(lead)} className="p-2 bg-white/10 rounded-lg hover:bg-white/20 transition" title="Details">👁</button>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
</div>
{/* Email Outreach Tab */}
{tab === "email" && (
<div className="space-y-6">
<div className="bg-blue-500/10 border border-blue-500/30 rounded-xl p-6">
<h3 className="text-lg font-bold mb-2">📧 Email Outreach</h3>
<p className="text-white/60 mb-4">Leads with email addresses. Click "Send" to open your email client with pre-filled template.</p>
<div className="grid gap-4">
{leads.filter(l => l.email).map(lead => (
<div key={lead.id} className="bg-white/5 rounded-lg p-4 flex items-center justify-between">
<div>
<p className="font-bold">{lead.name}</p>
<p className="text-sm text-white/60">{lead.email}</p>
<p className="text-xs text-white/40">{categoryIcons[lead.category]} {lead.category}</p>
</div>
<div className="flex gap-2">
<button onClick={() => updateStatus(lead.id, "contacted")} className="px-3 py-1 bg-yellow-500/20 text-yellow-400 rounded text-sm">Mark Emailed</button>
<a href={`mailto:${lead.email}?subject=Tu empleado IA 24/7 - SiteMente&body=Hola,%0D%0A%0D%0AVi tu negocio y me gustaría ofrecerte una solución que puede revolucionar tu atención al cliente.%0D%0A%0D%0ACon SiteMente tienes un empleado IA disponible 24/7 que:%0D%0A- Responde preguntas de clientes%0D%0A- Gestiona reservas automáticamente%0D%0A- Habla en español, inglés, francés, alemán...%0D%0A%0D%0A¿Te interesa ver una demo? Es gratis y sin compromiso.%0D%0A%0D%0ASaludos`} className="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg font-medium"> Send Email</a>
</div>
</div>
))}
{leads.filter(l => l.email).length === 0 && (
<p className="text-white/40 text-center py-8">No leads with email found. Research more leads!</p>
)}
</div>
</div>
<div className="bg-green-500/10 border border-green-500/30 rounded-xl p-6">
<h3 className="text-lg font-bold mb-2">🤖 AI Calling (Coming Soon)</h3>
<p className="text-white/60">Automated voice calls to leads - requires your confirmation before each call.</p>
<p className="text-sm text-white/40 mt-2">Integrating with Vapi - stay tuned!</p>
</div>
</div>
)}
</main>
{selectedLead && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-[#1a1625] border border-white/20 rounded-2xl p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-xl font-bold">{selectedLead.name}</h3>
<p className="text-white/60">{categoryIcons[selectedLead.category]} {selectedLead.category}</p>
</div>
<button onClick={() => setSelectedLead(null)} className="text-white/40 hover:text-white"></button>
</div>
<div className="space-y-4">
<div><label className="text-sm text-white/60">Address</label><p>{selectedLead.address}</p></div>
<div className="flex gap-4">
<div className="flex-1"><label className="text-sm text-white/60">Phone</label><p className={selectedLead.phone ? "text-green-400" : "text-red-400"}>{selectedLead.phone || "—"}</p></div>
<div className="flex-1"><label className="text-sm text-white/60">Website</label><p className={selectedLead.website ? "" : "text-red-400"}>{selectedLead.website || "—"}</p></div>
</div>
<div><label className="text-sm text-white/60">Score</label><p className="text-2xl font-bold text-brand-pink">{selectedLead.score}/10</p></div>
<div><label className="text-sm text-white/60">Notes</label><p>{selectedLead.notes}</p></div>
<div><label className="text-sm text-white/60">Next Action</label><p className="text-green-400">{selectedLead.nextAction}</p></div>
</div>
<div className="flex gap-2 mt-6">
{selectedLead.phone && <a href={"tel:" + selectedLead.phone} className="flex-1 py-2 bg-green-500 rounded-lg text-center font-medium">📞 Call</a>}
{selectedLead.email && <a href={"mailto:" + selectedLead.email} className="flex-1 py-2 bg-blue-500 rounded-lg text-center font-medium"> Email</a>}
<button onClick={() => { updateStatus(selectedLead.id, "won"); setSelectedLead(null); }} className="flex-1 py-2 bg-green-600 rounded-lg font-medium"> Won</button>
</div>
</div>
</div>
)}
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
# 📚 SiteMente Docs
Long-term documentation for SiteMente operations.
## 🚀 Getting Started
- [Quick Start Guide](/docs/quick-start)
- [Architecture Overview](/docs/architecture)
- [Environment Variables](/docs/env)
## 🛠 Products
- [Smart Starter](/docs/products/starter)
- [Smart Site](/docs/products/site)
- [AI Growth Partner](/docs/products/growth)
## 🔌 Integrations
- [Vapi Voice AI](/docs/integrations/vapi)
- [MiniMax AI](/docs/integrations/minimax)
- [Stripe Payments](/docs/integrations/stripe)
## 📋 Operational
- [Onboarding Checklist](/docs/ops/onboarding)
- [Troubleshooting Guide](/docs/ops/troubleshooting)
- [API Endpoints](/docs/ops/api)
---
*Auto-generated by Horus* 👁
+107 -1
View File
@@ -1,10 +1,116 @@
"use client";
import { useState, useEffect } from "react";
import MissionControlDashboard from "@/components/mission-control/MissionControlDashboard";
import { MissionControlProvider } from "@/lib/mission-control/store";
const CORRECT_USER = "Marshall";
const CORRECT_PASS = "#1284YallaHorus";
export default function MissionControlPage() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [mounted, setMounted] = useState(false);
useEffect(() => {
const saved = localStorage.getItem("sitemente:mc-auth");
if (saved === "true") {
setIsAuthenticated(true);
}
setMounted(true);
}, []);
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (username === CORRECT_USER && password === CORRECT_PASS) {
localStorage.setItem("sitemente:mc-auth", "true");
setIsAuthenticated(true);
setError("");
} else {
setError("Invalid credentials");
}
};
const handleLogout = () => {
localStorage.removeItem("sitemente:mc-auth");
setIsAuthenticated(false);
setUsername("");
setPassword("");
};
// Show loading until we've checked localStorage on mount
if (!mounted) {
return (
<div className="min-h-screen bg-[#1a1625] flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="text-5xl mb-4">👁</div>
<h1 className="text-3xl font-bold text-white mb-2">Mission Control</h1>
<p className="text-white/60">Loading...</p>
</div>
</div>
</div>
);
}
if (!isAuthenticated) {
return (
<div className="min-h-screen bg-[#1a1625] flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="text-5xl mb-4">👁</div>
<h1 className="text-3xl font-bold text-white mb-2">Mission Control</h1>
<p className="text-white/60">SiteMente Operations</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm text-white/70 mb-1">Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink"
placeholder="Enter username"
/>
</div>
<div>
<label className="block text-sm text-white/70 mb-1">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink"
placeholder="Enter password"
/>
</div>
{error && (
<p className="text-red-400 text-sm">{error}</p>
)}
<button
type="submit"
className="w-full py-3 bg-brand-pink rounded-lg font-semibold text-white hover:bg-[#ff7bc0] transition"
>
Access Control
</button>
</form>
<p className="text-center text-white/30 text-xs mt-6">
Restricted access. Authorized personnel only.
</p>
</div>
</div>
);
}
return (
<MissionControlProvider>
<MissionControlDashboard />
<MissionControlDashboard onLogout={handleLogout} />
</MissionControlProvider>
);
}
+94
View File
@@ -18,6 +18,13 @@ const fadeUp = {
type Language = "es" | "en";
type LeadFormData = {
name: string;
phone: string;
business: string;
type: string;
};
const contentByLang = {
es: {
nav: {
@@ -718,6 +725,8 @@ const servicesByLang = {
export default function HomePage() {
const [lang, setLang] = useState<Language>("es");
const [contactOpen, setContactOpen] = useState(false);
const [leadForm, setLeadForm] = useState<LeadFormData>({ name: "", phone: "", business: "", type: "restaurant" });
const [leadSubmitted, setLeadSubmitted] = useState(false);
const content = useMemo(() => contentByLang[lang], [lang]);
const heroSlides = useMemo<HeroSlide[]>(
() => [
@@ -891,6 +900,91 @@ export default function HomePage() {
))}
</motion.div>
{/* Lead Capture Form */}
{!leadSubmitted && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mt-12 max-w-md mx-auto"
>
<div className="bg-white/10 backdrop-blur rounded-xl p-6 border border-white/20">
<h3 className="text-lg font-bold mb-4 text-center">🚀 Pruébalo Ahora - Es Gratis</h3>
<p className="text-sm text-white/70 mb-4 text-center">Completa y te mostraremos tu AI en acción</p>
<form
onSubmit={(e) => {
e.preventDefault();
setLeadSubmitted(true);
// Here we'll connect to GHL later
}}
className="space-y-3"
>
<input
type="text"
placeholder="Tu nombre"
required
value={leadForm.name}
onChange={(e) => setLeadForm({ ...leadForm, name: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-white/50"
/>
<input
type="tel"
placeholder="Tu teléfono"
required
value={leadForm.phone}
onChange={(e) => setLeadForm({ ...leadForm, phone: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-white/50"
/>
<input
type="text"
placeholder="Nombre de tu negocio"
required
value={leadForm.business}
onChange={(e) => setLeadForm({ ...leadForm, business: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-white/50"
/>
<select
value={leadForm.type}
onChange={(e) => setLeadForm({ ...leadForm, type: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white"
>
<option value="restaurant" className="text-black">🍽 Restaurante</option>
<option value="real-estate" className="text-black">🏠 Inmobiliaria</option>
<option value="clinic" className="text-black">🏥 Clínica</option>
<option value="car-rental" className="text-black">🚗 Alquiler de coches</option>
<option value="other" className="text-black">📌 Otro</option>
</select>
<button
type="submit"
className="w-full bg-brand-pink hover:bg-[#ff7bc0] text-white font-bold py-3 rounded-lg transition"
>
Ver Demo AI Ahora
</button>
</form>
</div>
</motion.div>
)}
{leadSubmitted && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="mt-12 max-w-md mx-auto text-center"
>
<div className="bg-green-500/20 backdrop-blur rounded-xl p-8 border border-green-500/30">
<div className="text-5xl mb-4">🎉</div>
<h3 className="text-2xl font-bold mb-2">Perfecto, {leadForm.name}!</h3>
<p className="text-white/70 mb-4">Preparando tu demo personalizada...</p>
<a
href={`/demos?type=${leadForm.type}&name=${encodeURIComponent(leadForm.business)}`}
className="inline-block bg-brand-pink hover:bg-[#ff7bc0] text-white font-bold px-8 py-3 rounded-lg transition"
>
🚀 Entrar a Demo AI
</a>
</div>
</motion.div>
)}
</section>
</header>
+30
View File
@@ -0,0 +1,30 @@
import Link from "next/link";
export default function SuccessPage() {
return (
<div className="min-h-screen bg-[#5e4a8a] flex items-center justify-center p-4">
<div className="max-w-md w-full text-center">
<div className="text-6xl mb-6"></div>
<h1 className="text-3xl font-bold text-white mb-4">¡Pago Exitoso!</h1>
<p className="text-white/80 mb-8">
Thank you for your payment. We&apos;ve received your order and will contact you shortly to start setting up your AI solution.
</p>
<div className="bg-white/10 rounded-xl p-6 mb-8">
<h2 className="text-lg font-semibold text-white mb-2">What happens next?</h2>
<ul className="text-left text-white/70 space-y-2 text-sm">
<li> You&apos;ll receive a confirmation email</li>
<li> Our team will contact you within 24 hours</li>
<li> We&apos;ll schedule your onboarding call</li>
<li> Your AI solution will be live within 48 hours</li>
</ul>
</div>
<Link
href="/"
className="inline-block px-6 py-3 bg-brand-pink rounded-lg font-semibold text-white hover:bg-[#ff7bc0] transition"
>
Back to Home
</Link>
</div>
</div>
);
}