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.
This commit is contained in:
360
layouts/admin/single.html
Normal file
360
layouts/admin/single.html
Normal file
@@ -0,0 +1,360 @@
|
||||
{{ 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 }}
|
||||
|
||||
Reference in New Issue
Block a user