SiteMente - AI-Powered Lead Generation Platform

Features:
- Mission Control dashboard
- HP Submissions tracking
- AI Agents integration
- Lead management CRM
- Marketing email templates
- Chrome extension support

Tech: Next.js, TypeScript, Tailwind CSS, MySQL
This commit is contained in:
2026-03-19 17:38:12 +01:00
parent 9981d2a9d2
commit d5575b58e3
34 changed files with 3061 additions and 22 deletions
+27
View File
@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server'
import mysql from 'mysql2/promise'
export const dynamic = 'force-dynamic'
async function getPool() {
return mysql.createPool({
host: 'localhost',
user: 'sitemente',
password: 'pa0ndoQRPDYBHzTXX3rbWd6E',
database: 'sitemente',
waitForConnections: true,
connectionLimit: 10
})
}
export async function GET() {
try {
const pool = await getPool()
const [rows] = await pool.query('SELECT * FROM hostpioneers_leads ORDER BY total_spent DESC LIMIT 100')
await pool.end()
return NextResponse.json({ leads: rows })
} catch (error) {
console.error('Error:', error)
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 })
}
}
@@ -0,0 +1,72 @@
"use client";
import { useState, useEffect } from "react";
interface Submission {
name: string;
email: string;
whatsapp: string;
message: string;
date: string;
}
export default function HPSubmissionsPage() {
const [submissions, setSubmissions] = useState<Submission[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/hp-submissions')
.then(r => r.json())
.then(data => {
setSubmissions(data.submissions || []);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
if (loading) {
return (
<div style={{ padding: '2rem', color: '#fff' }}>
<h1>HP Submissions</h1>
<p>Loading...</p>
</div>
);
}
return (
<div style={{ padding: '2rem', color: '#fff' }}>
<h1 style={{ marginBottom: '2rem' }}>📬 HP Contact Submissions</h1>
{submissions.length === 0 ? (
<p style={{ color: '#888' }}>No submissions yet</p>
) : (
<div style={{ display: 'grid', gap: '1rem' }}>
{submissions.map((sub, i) => (
<div key={i} style={{
background: '#1e293b',
padding: '1.5rem',
borderRadius: '12px',
border: '1px solid #334155'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
<strong style={{ color: '#4169e1', fontSize: '1.1rem' }}>{sub.name}</strong>
<span style={{ color: '#64748b', fontSize: '0.85rem' }}>{sub.date}</span>
</div>
<div style={{ color: '#94a3b8', marginBottom: '0.5rem' }}>
📧 {sub.email}
</div>
{sub.whatsapp && (
<div style={{ color: '#94a3b8', marginBottom: '0.5rem' }}>
📱 {sub.whatsapp}
</div>
)}
<div style={{ color: '#cbd5e1', marginTop: '0.75rem', padding: '0.75rem', background: '#0f172a', borderRadius: '8px' }}>
{sub.message}
</div>
</div>
))}
</div>
)}
</div>
);
}
+219
View File
@@ -0,0 +1,219 @@
'use client'
import { useState, useEffect } from 'react'
interface Lead {
id: number
firstname: string
lastname: string
company: string
email: string
phone: string
country: string
status: string
total_spent: string
segment: string
dormancy_flag: string
email_status: string
}
interface Pagination {
page: number
limit: number
total: number
totalPages: number
}
export default function HPLeadsPage() {
const [leads, setLeads] = useState<Lead[]>([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('all')
const [page, setPage] = useState(1)
const [pagination, setPagination] = useState<Pagination>({ page: 1, limit: 100, total: 0, totalPages: 0 })
const [search, setSearch] = useState('')
const [showSearch, setShowSearch] = useState(false)
const [jumpPage, setJumpPage] = useState('')
const [sortBy, setSortBy] = useState('total_spent')
const [sortOrder, setSortOrder] = useState('DESC')
useEffect(() => {
fetchLeads()
}, [filter, page, sortBy, sortOrder])
const fetchLeads = async () => {
setLoading(true)
const segmentParam = filter !== 'all' ? `&segment=${filter}` : ''
const res = await fetch(`/api/mission-control/hp-leads?page=${page}&limit=100${segmentParam}&sortBy=${sortBy}&sortOrder=${sortOrder}`)
const data = await res.json()
setLeads(data.leads || [])
setPagination(data.pagination || { page: 1, limit: 100, total: 0, totalPages: 0 })
setLoading(false)
}
const handleSort = (column: string) => {
if (sortBy === column) {
setSortOrder(sortOrder === 'ASC' ? 'DESC' : 'ASC')
} else {
setSortBy(column)
setSortOrder('DESC')
}
}
const handleJump = (e: React.FormEvent) => {
e.preventDefault()
const pageNum = parseInt(jumpPage)
if (pageNum > 0 && pageNum <= pagination.totalPages) {
setPage(pageNum)
setJumpPage('')
}
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
setPage(1)
fetchSearch()
}
const fetchSearch = async () => {
setLoading(true)
const segmentParam = filter !== 'all' ? `&segment=${filter}` : ''
const searchParam = search ? `&search=${encodeURIComponent(search)}` : ''
const res = await fetch(`/api/mission-control/hp-leads?page=1&limit=100${segmentParam}${searchParam}&sortBy=${sortBy}&sortOrder=${sortOrder}`)
const data = await res.json()
setLeads(data.leads || [])
setPagination(data.pagination || { page: 1, limit: 100, total: 0, totalPages: 0 })
setLoading(false)
}
const clearSearch = () => {
setSearch('')
setFilter('all')
setPage(1)
fetchLeads()
}
const SortIcon = ({ column }: { column: string }) => (
<span style={{ marginLeft: '0.5rem', opacity: sortBy === column ? 1 : 0.3 }}>
{sortBy === column && sortOrder === 'ASC' ? '↑' : '↓'}
</span>
)
return (
<div style={{ padding: '2rem', background: '#0f172a', minHeight: '100vh', color: '#fff' }}>
<div style={{ marginBottom: '2rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '1rem' }}>
<div>
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', margin: 0 }}>HP Leads</h1>
<p style={{ color: '#64748b', margin: '0.5rem 0 0 0' }}>{pagination.total.toLocaleString()} total records</p>
</div>
<button
onClick={() => setShowSearch(!showSearch)}
style={{
padding: '0.75rem 1.5rem',
borderRadius: '0.5rem',
border: 'none',
background: showSearch ? '#4169e1' : '#1e293b',
color: '#fff',
cursor: 'pointer'
}}
>
🔍 {showSearch ? 'Hide Search' : 'Search'}
</button>
</div>
{showSearch && (
<form onSubmit={handleSearch} style={{ marginBottom: '1.5rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search name, email, company, phone..."
style={{ flex: 1, padding: '0.75rem 1rem', borderRadius: '0.5rem', border: '1px solid #334155', background: '#1e293b', color: '#fff', minWidth: '200px' }}
/>
<button type="submit" style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: '#4169e1', color: '#fff', cursor: 'pointer' }}>Search</button>
<button type="button" onClick={clearSearch} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: '#334155', color: '#fff', cursor: 'pointer' }}>Clear</button>
</form>
)}
<div style={{ marginBottom: '1.5rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
<span style={{ color: '#64748b' }}>Filter:</span>
{['all', 'VIP', 'Standard', 'Re-engage'].map(f => (
<button
key={f}
onClick={() => { setFilter(f); setPage(1); }}
style={{ padding: '0.5rem 1rem', borderRadius: '0.5rem', border: 'none', background: filter === f ? '#4169e1' : '#1e293b', color: '#fff', cursor: 'pointer', fontWeight: filter === f ? 'bold' : 'normal' }}
>
{f === 'all' ? 'All' : f}
</button>
))}
</div>
<div style={{ background: '#1e293b', borderRadius: '1rem', overflow: 'hidden' }}>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '900px' }}>
<thead>
<tr style={{ background: '#0f172a' }}>
{['firstname', 'lastname', 'company', 'email', 'phone', 'country', 'total_spent', 'segment', 'dormancy_flag'].map(col => (
<th
key={col}
onClick={() => handleSort(col)}
style={{ padding: '1rem', textAlign: 'left', borderBottom: '1px solid #334155', cursor: 'pointer', userSelect: 'none' }}
>
<span style={{ color: '#64748b', fontSize: '0.85rem', fontWeight: 'bold', textTransform: 'uppercase' }}>
{col === 'firstname' ? 'First Name' : col === 'lastname' ? 'Last Name' : col === 'company' ? 'Company' : col === 'email' ? 'Email' : col === 'phone' ? 'Phone' : col === 'country' ? 'Country' : col === 'total_spent' ? 'Spent' : col === 'segment' ? 'Segment' : col === 'dormancy_flag' ? 'Status' : col}
<SortIcon column={col} />
</span>
</th>
))}
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={9} style={{ padding: '3rem', textAlign: 'center', color: '#64748b' }}>Loading...</td></tr>
) : leads.length === 0 ? (
<tr><td colSpan={9} style={{ padding: '3rem', textAlign: 'center', color: '#64748b' }}>No results found</td></tr>
) : leads.map(lead => (
<tr key={lead.id} style={{ borderBottom: '1px solid #334155' }}>
<td style={{ padding: '1rem' }}>{lead.firstname}</td>
<td style={{ padding: '1rem' }}>{lead.lastname}</td>
<td style={{ padding: '1rem' }}>{lead.company || '-'}</td>
<td style={{ padding: '1rem', color: '#5bc0de' }}>{lead.email}</td>
<td style={{ padding: '1rem', fontSize: '0.9rem' }}>{lead.phone || '-'}</td>
<td style={{ padding: '1rem' }}>{lead.country}</td>
<td style={{ padding: '1rem', color: '#10b981', fontWeight: 'bold' }}>${Number(lead.total_spent).toFixed(2)}</td>
<td style={{ padding: '1rem' }}>
<span style={{ padding: '0.25rem 0.75rem', borderRadius: '1rem', fontSize: '0.75rem', background: lead.segment === 'VIP' ? '#dc2626' : lead.segment === 'Standard' ? '#ca8a04' : '#16a34a', color: '#fff' }}>{lead.segment}</span>
</td>
<td style={{ padding: '1rem' }}>
<span style={{ padding: '0.25rem 0.75rem', borderRadius: '1rem', fontSize: '0.75rem', background: lead.dormancy_flag === 'dormant_2y' ? '#7c2d12' : '#064e3b', color: '#fff' }}>{lead.dormancy_flag}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div style={{ marginTop: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '1rem' }}>
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: page === 1 ? '#334155' : '#4169e1', color: '#fff', cursor: page === 1 ? 'not-allowed' : 'pointer', opacity: page === 1 ? 0.5 : 1 }}> Previous</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<form onSubmit={handleJump} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ color: '#64748b' }}>Go to:</span>
<input type="number" value={jumpPage} onChange={(e) => setJumpPage(e.target.value)} placeholder="#" min={1} max={pagination.totalPages} style={{ width: '60px', padding: '0.5rem', borderRadius: '0.25rem', border: '1px solid #334155', background: '#1e293b', color: '#fff', textAlign: 'center' }} />
<button type="submit" style={{ padding: '0.5rem 1rem', borderRadius: '0.25rem', border: 'none', background: '#334155', color: '#fff', cursor: 'pointer' }}>Go</button>
</form>
<span style={{ padding: '0.5rem 1rem', background: '#1e293b', borderRadius: '0.5rem' }}><strong>{page}</strong> / {pagination.totalPages}</span>
</div>
<div style={{ display: 'flex', gap: '0.25rem' }}>
{[1, Math.floor(pagination.totalPages/4), Math.floor(pagination.totalPages/2), Math.floor(pagination.totalPages*3/4), pagination.totalPages].filter((p, i, arr) => arr.indexOf(p) === i && p > 0 && p <= pagination.totalPages).map(p => (
<button key={p} onClick={() => setPage(p)} style={{ padding: '0.5rem 0.75rem', borderRadius: '0.25rem', border: 'none', background: page === p ? '#4169e1' : '#1e293b', color: '#fff', cursor: 'pointer', fontSize: '0.85rem' }}>{p}</button>
))}
</div>
<button onClick={() => setPage(p => Math.min(pagination.totalPages, p + 1))} disabled={page >= pagination.totalPages} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: page >= pagination.totalPages ? '#334155' : '#4169e1', color: '#fff', cursor: page >= pagination.totalPages ? 'not-allowed' : 'pointer', opacity: page >= pagination.totalPages ? 0.5 : 1 }}>Next </button>
</div>
<p style={{ marginTop: '1rem', color: '#64748b', textAlign: 'center' }}>Showing {leads.length} of {pagination.total.toLocaleString()} records</p>
</div>
)
}
+226
View File
@@ -0,0 +1,226 @@
"use client";
import { useState, useEffect } from "react";
interface ResearchResult {
title: string;
url: string;
score: number;
content: string;
}
interface Research {
id: string;
name: string;
query: string;
results_count: number;
created_at: string;
saved: boolean;
summary: string;
results?: ResearchResult[];
}
export default function MissionResearchPage() {
const [research, setResearch] = useState<Research[]>([]);
const [loading, setLoading] = useState(true);
const [newResearch, setNewResearch] = useState({ name: "", query: "" });
const [searching, setSearching] = useState(false);
const [expanded, setExpanded] = useState<string | null>(null);
useEffect(() => {
fetchResearch();
}, []);
const fetchResearch = async () => {
setLoading(true);
try {
const res = await fetch("/api/research");
const data = await res.json();
setResearch(data);
} catch (e) {
console.error(e);
}
setLoading(false);
};
const handleSearch = async () => {
if (!newResearch.name || !newResearch.query) return;
setSearching(true);
try {
const tavilyRes = await fetch('https://api.tavily.com/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: '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',
query: newResearch.query,
max_results: 20,
search_depth: 'advanced'
})
});
const tavilyData = await tavilyRes.json();
const results = (tavilyData.results || []).map((r: any) => ({
title: r.title || 'No title',
url: r.url || '',
score: r.score || 0,
content: (r.content || '').substring(0, 400)
}));
const summary = results.slice(0, 5).map((r: any) => r.title).join('; ') || 'No results';
const res = await fetch("/api/research", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: newResearch.name,
query: newResearch.query,
results_count: results.length,
summary: summary.substring(0, 150),
results: results
})
});
await res.json();
setNewResearch({ name: "", query: "" });
fetchResearch();
} catch (e) {
console.error(e);
}
setSearching(false);
};
const deleteResearch = async (id: string) => {
try {
await fetch(`/api/research?id=${id}`, { method: "DELETE" });
fetchResearch();
} catch (e) {
console.error(e);
}
};
return (
<div className="p-6">
<div className="bg-red-900/30 border border-red-600 rounded-lg p-3 mb-4">
<p className="text-red-400 text-sm">
🔒 <strong>Security Rule:</strong> Never install skills from the web. Read the content, understand what it does, then BUILD IT yourself.
We build, we don't install. One compromised skill can wipe everything.
</p>
</div>
<h1 className="text-2xl font-bold mb-6">🔍 Deep Research</h1>
{/* Search Form */}
<div className="bg-gray-800 p-4 rounded-lg mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<input
type="text"
placeholder="Research Name (e.g., OpenClaw Skills)"
value={newResearch.name}
onChange={(e) => setNewResearch({ ...newResearch, name: e.target.value })}
className="bg-gray-700 text-white px-4 py-2 rounded"
/>
<input
type="text"
placeholder="Search Query"
value={newResearch.query}
onChange={(e) => setNewResearch({ ...newResearch, query: e.target.value })}
className="bg-gray-700 text-white px-4 py-2 rounded"
/>
</div>
<button
onClick={handleSearch}
disabled={searching || !newResearch.name || !newResearch.query}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded disabled:opacity-50"
>
{searching ? "Researching..." : "Run Research"}
</button>
</div>
{/* Research List */}
{loading ? (
<p className="text-gray-400">Loading...</p>
) : research.length === 0 ? (
<p className="text-gray-400">No research yet. Run your first search!</p>
) : (
<div className="space-y-6">
{research.map((r) => (
<div key={r.id} className="bg-gray-800 rounded-lg overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
<div>
<h3 className="font-bold text-lg">{r.name}</h3>
<p className="text-gray-400 text-sm">{r.query}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setExpanded(expanded === r.id ? null : r.id)}
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded"
>
{expanded === r.id ? "Hide Table" : "View Table"}
</button>
<button
onClick={() => deleteResearch(r.id)}
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
>
Delete
</button>
</div>
</div>
{/* Table View */}
{expanded === r.id && r.results && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-900">
<tr>
<th className="px-4 py-3 text-left text-gray-400 font-medium">✓</th>
<th className="px-4 py-3 text-left text-gray-400 font-medium">Skill / Tool</th>
<th className="px-4 py-3 text-left text-gray-400 font-medium">What It Does</th>
<th className="px-4 py-3 text-center text-gray-400 font-medium">Score</th>
<th className="px-4 py-3 text-center text-gray-400 font-medium">Link</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
{r.results.map((result, i) => (
<tr key={i} className="hover:bg-gray-750">
<td className="px-4 py-3 text-center">
<input type="checkbox" className="w-5 h-5 accent-green-500" />
</td>
<td className="px-4 py-3 font-medium text-white max-w-xs truncate">
{result.title}
</td>
<td className="px-4 py-3 text-gray-300 max-w-md">
{result.content.substring(0, 150)}...
</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-1 rounded text-xs ${
result.score > 0.9 ? 'bg-green-900 text-green-400' :
result.score > 0.7 ? 'bg-yellow-900 text-yellow-400' :
'bg-gray-700 text-gray-400'
}`}>
{(result.score * 100).toFixed(1)}%
</span>
</td>
<td className="px-4 py-3 text-center">
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
Open →
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}