From de5797ad418114f25cec3dcc4a67e24488de4e4f Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Sun, 8 Feb 2026 10:04:19 -0600 Subject: [PATCH] feat: add Content Tools page to Next.js admin - create user + import content (posts/quips/beacons) --- admin/src/app/content-tools/page.tsx | 478 +++++++++++++++++++++++++++ admin/src/components/Sidebar.tsx | 3 +- admin/src/lib/api.ts | 42 +++ 3 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 admin/src/app/content-tools/page.tsx diff --git a/admin/src/app/content-tools/page.tsx b/admin/src/app/content-tools/page.tsx new file mode 100644 index 0000000..b8bed5a --- /dev/null +++ b/admin/src/app/content-tools/page.tsx @@ -0,0 +1,478 @@ +'use client'; + +import AdminShell from '@/components/AdminShell'; +import { api } from '@/lib/api'; +import { useState } from 'react'; +import { UserPlus, Upload, AlertCircle, CheckCircle, Copy, FileText, Link2 } from 'lucide-react'; + +// ─── CSV Parser ─────────────────────────────────────── +function parseCsvLine(line: string): string[] { + const result: string[] = []; + let inQuotes = false; + let current = ''; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (ch === '"') { + inQuotes = !inQuotes; + } else if (ch === ',' && !inQuotes) { + result.push(current.trim()); + current = ''; + } else { + current += ch; + } + } + result.push(current.trim()); + return result; +} + +export default function ContentToolsPage() { + const [activeTab, setActiveTab] = useState<'create-user' | 'import'>('create-user'); + + return ( + +
+

Content Tools

+

Create users and import content

+
+ + {/* Tabs */} +
+ + +
+ + {activeTab === 'create-user' ? : } +
+ ); +} + +// ─── Create User Panel ──────────────────────────────── +function CreateUserPanel() { + const [form, setForm] = useState({ + email: '', + password: '', + handle: '', + display_name: '', + bio: '', + role: 'user', + verified: false, + official: false, + }); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null); + + const update = (key: string, value: any) => setForm((f) => ({ ...f, [key]: value })); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setResult(null); + try { + const resp = await api.adminCreateUser({ + ...form, + handle: form.handle.toLowerCase().trim(), + email: form.email.toLowerCase().trim(), + skip_email: true, + }); + setResult({ ok: true, message: `User created: @${resp.handle} (${resp.user_id})` }); + setForm({ email: '', password: '', handle: '', display_name: '', bio: '', role: 'user', verified: false, official: false }); + } catch (e: any) { + setResult({ ok: false, message: e.message || String(e) }); + } + setLoading(false); + }; + + return ( +
+

Create New User

+

+ Admin-created accounts are immediately active — no email verification required. +

+ +
+
+ + update('email', e.target.value)} + className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + placeholder="user@example.com" + /> +
+
+ + update('password', e.target.value)} + className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + placeholder="Min 8 characters" + /> +
+
+ +
+
+ + update('handle', e.target.value)} + className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + placeholder="username" + /> +
+
+ + update('display_name', e.target.value)} + className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + placeholder="John Doe" + /> +
+
+ +
+ +