Skip to content

Commit 3b1de87

Browse files
Add admin dashboard for lead management
- Created AdminDashboard component with Firebase and localStorage lead display - Added /admin route to App.tsx - Created admin-leads API endpoint to fetch Firebase data - Added CSV export functionality for both data sources Admin access: https://nativenodes.netlify.app/admin
1 parent e1b80f2 commit 3b1de87

File tree

4 files changed

+253
-0
lines changed

4 files changed

+253
-0
lines changed

anymate_system/anymate_landing-page/netlify.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
to = "/.netlify/functions/subscribe"
1111
status = 200
1212

13+
[[redirects]]
14+
from = "/api/admin-leads"
15+
to = "/.netlify/functions/admin-leads"
16+
status = 200
17+
1318
# SPA fallback for React Router
1419
[[redirects]]
1520
from = "/*"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// /.netlify/functions/admin-leads
2+
const admin = require("firebase-admin");
3+
4+
let appInitialized = false;
5+
function initAdmin() {
6+
if (appInitialized) return;
7+
const svc = process.env.FIREBASE_SERVICE_ACCOUNT;
8+
if (!svc) throw new Error("FIREBASE_SERVICE_ACCOUNT env var not set");
9+
const creds = JSON.parse(svc);
10+
if (!admin.apps.length) {
11+
admin.initializeApp({ credential: admin.credential.cert(creds) });
12+
}
13+
appInitialized = true;
14+
}
15+
16+
exports.handler = async function (event) {
17+
// CORS headers
18+
const headers = {
19+
"Access-Control-Allow-Origin": "*",
20+
"Access-Control-Allow-Methods": "GET, OPTIONS",
21+
"Access-Control-Allow-Headers": "Content-Type"
22+
};
23+
24+
// CORS preflight
25+
if (event.httpMethod === "OPTIONS") {
26+
return { statusCode: 204, headers, body: "" };
27+
}
28+
29+
if (event.httpMethod !== "GET") {
30+
return { statusCode: 405, headers, body: "Method Not Allowed" };
31+
}
32+
33+
try {
34+
initAdmin();
35+
const db = admin.firestore();
36+
37+
// Fetch all leads from waitlist collection
38+
const snapshot = await db.collection("waitlist").orderBy("ts", "desc").get();
39+
const leads = [];
40+
41+
snapshot.forEach(doc => {
42+
const data = doc.data();
43+
leads.push({
44+
id: doc.id,
45+
name: data.name || "",
46+
email: data.email || "",
47+
role: data.role || "Unknown",
48+
first_use: data.first_use || "",
49+
ts: data.ts ? data.ts.toDate().toISOString() : new Date().toISOString()
50+
});
51+
});
52+
53+
return {
54+
statusCode: 200,
55+
headers,
56+
body: JSON.stringify({
57+
leads,
58+
count: leads.length
59+
})
60+
};
61+
} catch (err) {
62+
console.error("Admin leads error:", err);
63+
return {
64+
statusCode: 500,
65+
headers,
66+
body: JSON.stringify({
67+
error: "Failed to fetch leads",
68+
leads: []
69+
})
70+
};
71+
}
72+
};

anymate_system/anymate_landing-page/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AgentInterface from './pages/AgentInterface';
66
import NotFound from './pages/NotFound';
77
import Waitlist from './components/Waitlist';
88
import ActivatePanel from './components/ActivatePanel';
9+
import { AdminDashboard } from './components/AdminDashboard';
910

1011
function HeaderBar(){
1112
const [open, setOpen] = useState(false);
@@ -48,6 +49,7 @@ function App() {
4849
<Route path="/pathsassin-3d" element={<PATHsassin3D />} />
4950
<Route path="/agent" element={<AgentInterface />} />
5051
<Route path="/waitlist" element={<Waitlist />} />
52+
<Route path="/admin" element={<AdminDashboard />} />
5153
<Route path="*" element={<NotFound />} />
5254
</Route>
5355
</Routes>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Card, CardHeader, CardTitle, CardContent } from './card';
3+
import { Badge } from './badge';
4+
5+
interface Lead {
6+
name: string;
7+
email: string;
8+
role: 'Creator' | 'Buyer' | 'Both';
9+
first_use: string;
10+
ts: string;
11+
}
12+
13+
export function AdminDashboard() {
14+
const [leads, setLeads] = useState<Lead[]>([]);
15+
const [firebaseLeads, setFirebaseLeads] = useState<Lead[]>([]);
16+
const [loading, setLoading] = useState(true);
17+
18+
useEffect(() => {
19+
// Load localStorage leads
20+
try {
21+
const localLeads = JSON.parse(localStorage.getItem('anym8_waitlist') || '[]');
22+
setLeads(localLeads);
23+
} catch (e) {
24+
console.log('No local leads found');
25+
}
26+
27+
// Try to fetch Firebase leads
28+
fetchFirebaseLeads();
29+
}, []);
30+
31+
const fetchFirebaseLeads = async () => {
32+
try {
33+
const response = await fetch('/api/admin-leads');
34+
if (response.ok) {
35+
const data = await response.json();
36+
setFirebaseLeads(data.leads || []);
37+
}
38+
} catch (e) {
39+
console.log('Firebase leads not available');
40+
} finally {
41+
setLoading(false);
42+
}
43+
};
44+
45+
const downloadCSV = (data: Lead[], filename: string) => {
46+
const csv = ['name,email,role,first_use,timestamp']
47+
.concat(data.map(lead =>
48+
[lead.name, lead.email, lead.role, lead.first_use, lead.ts]
49+
.map(v => `"${String(v || '').replace('"', '""')}"`)
50+
.join(',')
51+
))
52+
.join('\n');
53+
54+
const blob = new Blob([csv], { type: 'text/csv' });
55+
const url = URL.createObjectURL(blob);
56+
const a = document.createElement('a');
57+
a.href = url;
58+
a.download = filename;
59+
a.click();
60+
URL.revokeObjectURL(url);
61+
};
62+
63+
if (loading) {
64+
return <div className="p-8">Loading admin dashboard...</div>;
65+
}
66+
67+
return (
68+
<div className="min-h-screen bg-black text-white p-8">
69+
<div className="max-w-6xl mx-auto">
70+
<h1 className="text-3xl font-bold mb-8">ANYM⁸ Admin Dashboard</h1>
71+
72+
<div className="grid gap-6">
73+
{/* Firebase Leads */}
74+
<Card className="bg-black/60 border-white/20">
75+
<CardHeader>
76+
<CardTitle className="flex justify-between items-center">
77+
Firebase Leads ({firebaseLeads.length})
78+
<button
79+
onClick={() => downloadCSV(firebaseLeads, 'firebase-leads.csv')}
80+
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 rounded-lg text-sm"
81+
>
82+
Download CSV
83+
</button>
84+
</CardTitle>
85+
</CardHeader>
86+
<CardContent>
87+
{firebaseLeads.length > 0 ? (
88+
<div className="overflow-x-auto">
89+
<table className="w-full text-sm">
90+
<thead>
91+
<tr className="border-b border-white/10">
92+
<th className="text-left p-2">Name</th>
93+
<th className="text-left p-2">Email</th>
94+
<th className="text-left p-2">Role</th>
95+
<th className="text-left p-2">First Use</th>
96+
<th className="text-left p-2">Date</th>
97+
</tr>
98+
</thead>
99+
<tbody>
100+
{firebaseLeads.map((lead, i) => (
101+
<tr key={i} className="border-b border-white/5">
102+
<td className="p-2">{lead.name}</td>
103+
<td className="p-2">{lead.email}</td>
104+
<td className="p-2">
105+
<Badge variant="secondary">{lead.role}</Badge>
106+
</td>
107+
<td className="p-2 text-xs text-white/70">{lead.first_use}</td>
108+
<td className="p-2 text-xs text-white/70">
109+
{new Date(lead.ts).toLocaleDateString()}
110+
</td>
111+
</tr>
112+
))}
113+
</tbody>
114+
</table>
115+
</div>
116+
) : (
117+
<p className="text-white/60">No Firebase leads found. Check API connection.</p>
118+
)}
119+
</CardContent>
120+
</Card>
121+
122+
{/* Local Backup Leads */}
123+
<Card className="bg-black/60 border-white/20">
124+
<CardHeader>
125+
<CardTitle className="flex justify-between items-center">
126+
Local Backup Leads ({leads.length})
127+
<button
128+
onClick={() => downloadCSV(leads, 'local-backup-leads.csv')}
129+
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm"
130+
>
131+
Download CSV
132+
</button>
133+
</CardTitle>
134+
</CardHeader>
135+
<CardContent>
136+
{leads.length > 0 ? (
137+
<div className="overflow-x-auto">
138+
<table className="w-full text-sm">
139+
<thead>
140+
<tr className="border-b border-white/10">
141+
<th className="text-left p-2">Name</th>
142+
<th className="text-left p-2">Email</th>
143+
<th className="text-left p-2">Role</th>
144+
<th className="text-left p-2">First Use</th>
145+
<th className="text-left p-2">Date</th>
146+
</tr>
147+
</thead>
148+
<tbody>
149+
{leads.map((lead, i) => (
150+
<tr key={i} className="border-b border-white/5">
151+
<td className="p-2">{lead.name}</td>
152+
<td className="p-2">{lead.email}</td>
153+
<td className="p-2">
154+
<Badge variant="secondary">{lead.role}</Badge>
155+
</td>
156+
<td className="p-2 text-xs text-white/70">{lead.first_use}</td>
157+
<td className="p-2 text-xs text-white/70">
158+
{new Date(lead.ts).toLocaleDateString()}
159+
</td>
160+
</tr>
161+
))}
162+
</tbody>
163+
</table>
164+
</div>
165+
) : (
166+
<p className="text-white/60">No local backup leads found.</p>
167+
)}
168+
</CardContent>
169+
</Card>
170+
</div>
171+
</div>
172+
</div>
173+
);
174+
}

0 commit comments

Comments
 (0)