Files
hydro-site-final/layouts/admin/single.html
Dr. Jones d9961a76ad Initial commit: site code, layouts, content (images added separately)
Fixes duplicate placeholder cover image on the 3 newest blog posts,
broken aspect-ratio classes on the albums listing page, and a
non-responsive fixed sidebar on the chat page.
2026-07-08 06:18:17 +00:00

361 lines
16 KiB
HTML

{{ define "main" }}
<div class="bg-gray-900 min-h-screen py-16 px-4 sm:px-6 lg:px-8" x-data="adminPanel()" x-init="init()">
<div class="max-w-6xl mx-auto">
<!-- Login Screen -->
<div x-show="!authenticated" class="max-w-md mx-auto">
<div class="bg-gray-800 rounded-lg p-8 border border-gray-700">
<h1 class="text-3xl font-bold text-gray-100 mb-6 text-center">Admin Login</h1>
<form @submit.prevent="adminLogin()" class="space-y-6">
<div>
<label for="admin-password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
<input type="password" id="admin-password" x-model="adminPassword" required
class="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div x-show="loginError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200">
<p x-text="loginError"></p>
</div>
<button type="submit"
:disabled="loginLoading"
class="w-full px-6 py-3 bg-primary hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50">
<span x-show="!loginLoading">Login</span>
<span x-show="loginLoading">Logging in...</span>
</button>
</form>
</div>
</div>
<!-- Admin Dashboard -->
<div x-show="authenticated" x-transition>
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-100">Admin Dashboard</h1>
<button @click="logout()" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Logout
</button>
</div>
<!-- Tabs -->
<div class="border-b border-gray-700 mb-6 flex">
<button @click="activeTab = 'members'; loadPendingMembers()"
:class="activeTab === 'members' ? 'border-b-2 border-primary text-primary' : 'text-gray-400 hover:text-gray-300'"
class="px-6 py-3 font-medium transition-colors">
Pending Members
</button>
<button @click="activeTab = 'testimonials'; loadTestimonials()"
:class="activeTab === 'testimonials' ? 'border-b-2 border-primary text-primary' : 'text-gray-400 hover:text-gray-300'"
class="px-6 py-3 font-medium transition-colors">
Testimonials
</button>
</div>
<!-- Pending Members Tab -->
<div x-show="activeTab === 'members'" x-transition>
<div x-show="loadingMembers" class="text-center text-gray-400 py-12">
Loading...
</div>
<div x-show="membersError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200 mb-6">
<p x-text="membersError"></p>
</div>
<div class="space-y-4">
<template x-for="member in pendingMembers" :key="member.id">
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold text-gray-100" x-text="member.name"></h3>
<p class="text-gray-400" x-text="member.email"></p>
<p class="text-sm text-gray-500 mt-2" x-text="'Signed up: ' + new Date(member.created_at).toLocaleString()"></p>
</div>
<div class="flex gap-3">
<button @click="approveMember(member.id)"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
Approve
</button>
<button @click="denyMember(member.id)"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Deny
</button>
</div>
</div>
</div>
</template>
<div x-show="!loadingMembers && !membersError && pendingMembers.length === 0" class="text-center text-gray-400 py-12">
<p>No pending memberships</p>
</div>
</div>
</div>
<!-- Testimonials Tab -->
<div x-show="activeTab === 'testimonials'" x-transition>
<div x-show="loadingTestimonials" class="text-center text-gray-400 py-12">
Loading...
</div>
<div x-show="testimonialsError" class="bg-red-900/50 border border-red-700 rounded-lg p-4 text-red-200 mb-6">
<p x-text="testimonialsError"></p>
</div>
<div class="space-y-4">
<template x-for="testimonial in allTestimonials" :key="testimonial.id">
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-100" x-text="testimonial.author_name"></h3>
<p class="text-sm text-gray-400" x-text="new Date(testimonial.created_at).toLocaleString()"></p>
<span x-show="testimonial.approved" class="inline-block mt-2 px-2 py-1 bg-green-600/20 text-green-300 text-xs rounded">
Approved
</span>
<span x-show="!testimonial.approved" class="inline-block mt-2 px-2 py-1 bg-yellow-600/20 text-yellow-300 text-xs rounded">
Pending
</span>
</div>
<div class="flex gap-3">
<button x-show="!testimonial.approved"
@click="approveTestimonial(testimonial.id)"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
Approve
</button>
<button @click="deleteTestimonial(testimonial.id)"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Delete
</button>
</div>
</div>
<p class="text-gray-300 leading-relaxed" x-text="testimonial.content"></p>
</div>
</template>
<div x-show="!loadingTestimonials && !testimonialsError && allTestimonials.length === 0" class="text-center text-gray-400 py-12">
<p>No testimonials</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const getAdminApiBase = () => {
const host = window.location.hostname;
const isLocal = host === 'localhost' || host === '127.0.0.1' || host.startsWith('10.');
return isLocal ? 'http://' + host + ':5001' : window.location.origin.replace(/\/$/, '');
};
const ADMIN_API_BASE = getAdminApiBase() + '/api';
function adminPanel() {
return {
authenticated: false,
adminToken: null,
adminPassword: '',
loginError: '',
loginLoading: false,
activeTab: 'members',
pendingMembers: [],
loadingMembers: false,
membersError: '',
allTestimonials: [],
loadingTestimonials: false,
testimonialsError: '',
init() {
const token = localStorage.getItem('admin_token');
if (token) {
this.adminToken = token;
this.authenticated = true;
this.loadPendingMembers();
}
},
async adminLogin() {
this.loginError = '';
this.loginLoading = true;
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: this.adminPassword })
});
const data = await response.json();
this.loginLoading = false;
if (response.ok) {
this.adminToken = data.token;
localStorage.setItem('admin_token', data.token);
this.authenticated = true;
this.adminPassword = '';
this.loadPendingMembers();
} else {
this.loginError = data.error || 'Login failed';
}
} catch (error) {
this.loginLoading = false;
this.loginError = 'Login failed. Please try again.';
}
},
logout() {
localStorage.removeItem('admin_token');
this.adminToken = null;
this.authenticated = false;
},
async loadPendingMembers() {
this.loadingMembers = true;
this.membersError = '';
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/pending`, {
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
this.loadingMembers = false;
if (response.ok) {
this.pendingMembers = data.members || [];
} else {
if (response.status === 401) {
this.logout();
this.loginError = 'Session expired. Please login again.';
} else {
this.membersError = data.error || 'Failed to load members';
}
}
} catch (error) {
this.loadingMembers = false;
this.membersError = 'Failed to load members';
}
},
async approveMember(userId) {
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.adminToken}`
},
body: JSON.stringify({ user_id: userId })
});
const data = await response.json();
if (response.ok) {
this.loadPendingMembers();
alert('Member approved successfully');
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to approve member');
}
}
} catch (error) {
alert('Failed to approve member');
}
},
async denyMember(userId) {
if (!confirm('Are you sure you want to deny this membership?')) return;
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/deny`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.adminToken}`
},
body: JSON.stringify({ user_id: userId })
});
const data = await response.json();
if (response.ok) {
this.loadPendingMembers();
alert('Member denied');
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to deny member');
}
}
} catch (error) {
alert('Failed to deny member');
}
},
async loadTestimonials() {
this.loadingTestimonials = true;
this.testimonialsError = '';
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials`, {
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
this.loadingTestimonials = false;
if (response.ok) {
this.allTestimonials = data.testimonials || [];
} else {
if (response.status === 401) {
this.logout();
} else {
this.testimonialsError = data.error || 'Failed to load testimonials';
}
}
} catch (error) {
this.loadingTestimonials = false;
this.testimonialsError = 'Failed to load testimonials';
}
},
async approveTestimonial(testimonialId) {
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials/${testimonialId}/approve`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
if (response.ok) {
this.loadTestimonials();
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to approve testimonial');
}
}
} catch (error) {
alert('Failed to approve testimonial');
}
},
async deleteTestimonial(testimonialId) {
if (!confirm('Are you sure you want to delete this testimonial?')) return;
try {
const response = await fetch(`${ADMIN_API_BASE}/admin/testimonials/${testimonialId}/delete`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.adminToken}` }
});
const data = await response.json();
if (response.ok) {
this.loadTestimonials();
} else {
if (response.status === 401) {
this.logout();
} else {
alert(data.error || 'Failed to delete testimonial');
}
}
} catch (error) {
alert('Failed to delete testimonial');
}
}
}
}
</script>
{{ end }}