initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,300 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
interface LegacyUser {
id: string;
wp_id: number;
wp_username: string;
email: string;
display_name: string | null;
role: string;
is_active: boolean;
auth_user_id: string | null;
password_upgraded: boolean;
imported_at: string;
last_login_at: string | null;
notes: string | null;
}
const ROLE_COLORS: Record<string, string> = {
administrator: 'text-red-400 bg-red-400/10',
editor: 'text-purple-400 bg-purple-400/10',
author: 'text-blue-400 bg-blue-400/10',
contributor: 'text-green-400 bg-green-400/10',
reader: 'text-gray-400 bg-gray-400/10',
};
export default function WPUsersAdminPage() {
const { user, loading } = useAuth();
const router = useRouter();
const supabase = createClient();
const [users, setUsers] = useState<LegacyUser[]>([]);
const [loadingData, setLoadingData] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
const showNotification = (type: 'success' | 'error', message: string) => {
setNotification({ type, message });
setTimeout(() => setNotification(null), 4000);
};
useEffect(() => {
if (!loading && !user) router.push('/login');
}, [user, loading, router]);
useEffect(() => {
if (user) fetchUsers();
}, [user]);
const fetchUsers = async () => {
setLoadingData(true);
try {
const { data, error } = await supabase
.from('wp_legacy_users')
.select('*')
.order('wp_id', { ascending: true });
if (error) throw error;
setUsers(data || []);
} catch (err: any) {
showNotification('error', err.message || 'Failed to load users');
} finally {
setLoadingData(false);
}
};
const toggleActive = async (userId: string, currentActive: boolean) => {
setProcessingIds(prev => new Set(prev).add(userId));
try {
const { error } = await supabase
.from('wp_legacy_users')
.update({ is_active: !currentActive })
.eq('id', userId);
if (error) throw error;
setUsers(prev => prev.map(u => u.id === userId ? { ...u, is_active: !currentActive } : u));
showNotification('success', `User ${!currentActive ? 'activated' : 'deactivated'} successfully.`);
} catch (err: any) {
showNotification('error', err.message);
} finally {
setProcessingIds(prev => { const s = new Set(prev); s.delete(userId); return s; });
}
};
const updateRole = async (userId: string, newRole: string) => {
setProcessingIds(prev => new Set(prev).add(userId));
try {
const { error } = await supabase
.from('wp_legacy_users')
.update({ role: newRole })
.eq('id', userId);
if (error) throw error;
setUsers(prev => prev.map(u => u.id === userId ? { ...u, role: newRole } : u));
showNotification('success', 'Role updated successfully.');
} catch (err: any) {
showNotification('error', err.message);
} finally {
setProcessingIds(prev => { const s = new Set(prev); s.delete(userId); return s; });
}
};
const filteredUsers = users.filter(u => {
const matchesSearch = !searchQuery ||
u.wp_username.toLowerCase().includes(searchQuery.toLowerCase()) ||
u.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
(u.display_name || '').toLowerCase().includes(searchQuery.toLowerCase());
const matchesRole = roleFilter === 'all' || u.role === roleFilter;
const matchesStatus = statusFilter === 'all' ||
(statusFilter === 'active' && u.is_active) ||
(statusFilter === 'inactive' && !u.is_active) ||
(statusFilter === 'migrated' && !!u.auth_user_id) ||
(statusFilter === 'pending' && !u.auth_user_id);
return matchesSearch && matchesRole && matchesStatus;
});
const stats = {
total: users.length,
active: users.filter(u => u.is_active).length,
migrated: users.filter(u => !!u.auth_user_id).length,
admins: users.filter(u => u.role === 'administrator').length,
};
return (
<div className="min-h-screen bg-[#0a0a0a] text-white p-6">
{notification && (
<div className={`fixed top-4 right-4 z-50 px-5 py-3 rounded-lg text-sm font-medium shadow-lg ${
notification.type === 'success' ? 'bg-green-500/20 border border-green-500/40 text-green-300' : 'bg-red-500/20 border border-red-500/40 text-red-300'
}`}>
{notification.message}
</div>
)}
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<div className="flex items-center gap-3 mb-1">
<Link href="/admin" className="text-[#555] hover:text-white text-sm transition-colors"> Admin</Link>
<span className="text-[#333]">/</span>
<span className="text-[#888] text-sm">WordPress User Migration</span>
</div>
<h1 className="text-2xl font-bold text-white">WordPress Migrated Users</h1>
<p className="text-[#555] text-sm mt-1">
All users imported from the WordPress broadcas_broad database. Passwords validated via phpass on first login.
</p>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-4 mb-6">
{[
{ label: 'Total Users', value: stats.total, color: 'text-white' },
{ label: 'Active', value: stats.active, color: 'text-green-400' },
{ label: 'Migrated to Auth', value: stats.migrated, color: 'text-blue-400' },
{ label: 'Administrators', value: stats.admins, color: 'text-red-400' },
].map(s => (
<div key={s.label} className="bg-[#111] border border-[#222] rounded-xl p-4">
<div className={`text-2xl font-bold ${s.color}`}>{s.value}</div>
<div className="text-[#555] text-xs mt-1">{s.label}</div>
</div>
))}
</div>
{/* Filters */}
<div className="flex gap-3 mb-5 flex-wrap">
<input
type="text"
placeholder="Search username, email, name..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="flex-1 min-w-48 bg-[#111] border border-[#333] rounded-lg px-4 py-2 text-white placeholder-[#444] focus:outline-none focus:border-[#c9a84c] text-sm"
/>
<select
value={roleFilter}
onChange={e => setRoleFilter(e.target.value)}
className="bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#c9a84c]"
>
<option value="all">All Roles</option>
<option value="administrator">Administrator</option>
<option value="editor">Editor</option>
<option value="author">Author</option>
<option value="contributor">Contributor</option>
</select>
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
className="bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#c9a84c]"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="migrated">Migrated to Auth</option>
<option value="pending">Pending Migration</option>
</select>
</div>
{/* Table */}
{loadingData ? (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-2 border-[#c9a84c] border-t-transparent rounded-full animate-spin" />
</div>
) : (
<div className="bg-[#111] border border-[#222] rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#222]">
<th className="text-left px-4 py-3 text-[#555] font-medium">WP ID</th>
<th className="text-left px-4 py-3 text-[#555] font-medium">Username</th>
<th className="text-left px-4 py-3 text-[#555] font-medium">Email</th>
<th className="text-left px-4 py-3 text-[#555] font-medium">Display Name</th>
<th className="text-left px-4 py-3 text-[#555] font-medium">Role</th>
<th className="text-left px-4 py-3 text-[#555] font-medium">Status</th>
<th className="text-left px-4 py-3 text-[#555] font-medium">Auth</th>
<th className="text-left px-4 py-3 text-[#555] font-medium">Actions</th>
</tr>
</thead>
<tbody>
{filteredUsers.map(u => (
<tr key={u.id} className="border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors">
<td className="px-4 py-3 text-[#555] font-mono text-xs">{u.wp_id}</td>
<td className="px-4 py-3">
<span className="text-white font-medium">{u.wp_username}</span>
</td>
<td className="px-4 py-3 text-[#888] text-xs">{u.email}</td>
<td className="px-4 py-3 text-[#888] text-xs">{u.display_name || '—'}</td>
<td className="px-4 py-3">
<select
value={u.role}
onChange={e => updateRole(u.id, e.target.value)}
disabled={processingIds.has(u.id)}
className={`text-xs px-2 py-1 rounded-full font-medium border-0 focus:outline-none cursor-pointer ${ROLE_COLORS[u.role] || 'text-gray-400 bg-gray-400/10'}`}
>
<option value="administrator">Administrator</option>
<option value="editor">Editor</option>
<option value="author">Author</option>
<option value="contributor">Contributor</option>
<option value="reader">Reader</option>
</select>
</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${u.is_active ? 'text-green-400 bg-green-400/10' : 'text-red-400 bg-red-400/10'}`}>
{u.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-4 py-3">
{u.auth_user_id ? (
<span className="text-xs text-blue-400"> Migrated</span>
) : (
<span className="text-xs text-[#444]">Pending</span>
)}
{u.password_upgraded && (
<span className="text-xs text-green-400 ml-2">🔒 Upgraded</span>
)}
</td>
<td className="px-4 py-3">
<button
onClick={() => toggleActive(u.id, u.is_active)}
disabled={processingIds.has(u.id)}
className={`text-xs px-3 py-1 rounded-lg transition-colors disabled:opacity-50 ${
u.is_active
? 'bg-red-500/10 text-red-400 hover:bg-red-500/20' :'bg-green-500/10 text-green-400 hover:bg-green-500/20'
}`}
>
{u.is_active ? 'Deactivate' : 'Activate'}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="px-4 py-3 border-t border-[#222] text-[#555] text-xs">
Showing {filteredUsers.length} of {users.length} users
</div>
</div>
)}
{/* Info box */}
<div className="mt-6 bg-[#111] border border-[#333] rounded-xl p-5">
<h3 className="text-white font-medium text-sm mb-3">📋 Migration Notes</h3>
<ul className="space-y-1.5 text-[#555] text-xs">
<li> Passwords are validated using phpass on first login. Both <code className="text-[#888]">$P$B</code> and <code className="text-[#888]">$wp$2y$10$</code> formats are supported.</li>
<li> On successful first login, a Supabase auth account is created and the password is transparently upgraded.</li>
<li> Test accounts (IDs 757, 760, 761) are set to <strong className="text-red-400">Inactive</strong> and cannot log in until manually reactivated.</li>
<li> ryansalazar (ID 21) has been upgraded to <strong className="text-red-400">Administrator</strong> per site owner instructions.</li>
<li> deanna.jones (ID 728) has been set to <strong className="text-red-400">Administrator</strong> for full access.</li>
<li> The Featured category is locked Contributors and Authors cannot post to it. The trigger strips it automatically on save.</li>
</ul>
</div>
</div>
</div>
);
}