Updated admin panel, replaced with plain html and alpine js
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
// Main application state and logic
|
||||
function app() {
|
||||
return {
|
||||
state: {
|
||||
token: null,
|
||||
password: '',
|
||||
loading: false,
|
||||
isProcessing: false,
|
||||
statusMessage: '',
|
||||
mediaQueue: [],
|
||||
currentMedia: null
|
||||
},
|
||||
|
||||
init() {
|
||||
// Check for existing token on component mount
|
||||
this.checkExistingToken();
|
||||
|
||||
// If we have a token, start filling the media queue
|
||||
if (this.state.token) {
|
||||
// Use setTimeout to ensure Alpine is fully initialized
|
||||
setTimeout(() => {
|
||||
this.fillMediaQueue();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
|
||||
checkExistingToken() {
|
||||
const savedToken = localStorage.getItem('adminToken');
|
||||
if (savedToken) {
|
||||
try {
|
||||
// Parse JWT payload to check for expiration
|
||||
const payload = JSON.parse(atob(savedToken.split('.')[1]));
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (payload.exp && payload.exp > currentTime) {
|
||||
this.state.token = savedToken;
|
||||
} else {
|
||||
// Token expired, remove it
|
||||
localStorage.removeItem('adminToken');
|
||||
}
|
||||
} catch (error) {
|
||||
// Invalid token format, remove it
|
||||
localStorage.removeItem('adminToken');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
if (!this.state.password.trim()) {
|
||||
this.showToast("Error", "Please enter a password", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.loading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ Password: this.state.password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.token) {
|
||||
this.state.token = data.token;
|
||||
localStorage.setItem('adminToken', data.token);
|
||||
this.showToast("Success", data.message || "Login successful");
|
||||
this.state.password = '';
|
||||
} else {
|
||||
this.showToast("Error", data.message || "Login failed", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showToast("Error", "Network error. Please try again.", "error");
|
||||
} finally {
|
||||
this.state.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
handleGiteaLogin() {
|
||||
// Redirect to the Gitea OAuth endpoint
|
||||
window.location.href = '/admin/api/login/oauth/gitea';
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
this.state.token = null;
|
||||
localStorage.removeItem('adminToken');
|
||||
},
|
||||
|
||||
async fetchMedia() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/getmedia', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.state.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
this.showToast("Session Expired", "Please login again", "error");
|
||||
this.handleLogout();
|
||||
// Redirect to login page after logout
|
||||
setTimeout(() => {
|
||||
// Trigger a page reload to update the UI based on state
|
||||
location.reload();
|
||||
}, 1500);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
this.showToast("Error", errorData.message || "Failed to fetch media", "error");
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.showToast("Error", "Network error. Please try again.", "error");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchNextMedia() {
|
||||
const media = await this.fetchMedia();
|
||||
if (media) {
|
||||
this.state.mediaQueue.push(media);
|
||||
|
||||
// Update current media if it's the first item
|
||||
if (this.state.mediaQueue.length === 1) {
|
||||
this.state.currentMedia = media;
|
||||
}
|
||||
|
||||
// Preload next items (up to 5 in queue total)
|
||||
if (this.state.mediaQueue.length < 5) {
|
||||
this.preloadMedia();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fillMediaQueue() {
|
||||
// Don't fetch if already loading or queue is full
|
||||
if (this.state.loading || this.state.mediaQueue.length >= 5) return;
|
||||
|
||||
// Determine how many more items we need
|
||||
const itemsNeeded = 5 - this.state.mediaQueue.length;
|
||||
|
||||
// Fetch items one by one
|
||||
for (let i = 0; i < itemsNeeded; i++) {
|
||||
await this.fetchNextMedia();
|
||||
}
|
||||
},
|
||||
|
||||
preloadMedia() {
|
||||
// Preload all items after the first one in the queue
|
||||
this.state.mediaQueue.slice(1, 5).forEach(media => {
|
||||
if (!media.preview_url) return;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// Successful preload - nothing to do
|
||||
};
|
||||
img.onerror = () => {
|
||||
// This media is broken even before we show it: remove it now
|
||||
this.state.mediaQueue = this.state.mediaQueue.filter(item => item.id !== media.id);
|
||||
// Update current media if needed
|
||||
if (this.state.currentMedia && this.state.currentMedia.id === media.id) {
|
||||
this.state.currentMedia = this.state.mediaQueue.length > 0 ? this.state.mediaQueue[0] : null;
|
||||
}
|
||||
};
|
||||
img.src = media.preview_url;
|
||||
});
|
||||
},
|
||||
|
||||
async handleAction(action) {
|
||||
if (!this.state.currentMedia) return;
|
||||
|
||||
this.state.isProcessing = true;
|
||||
this.state.statusMessage = `${action === 'approve' ? 'Approving' : 'Rejecting'}...`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/${action}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.state.token}`,
|
||||
},
|
||||
body: JSON.stringify({ mediaId: this.state.currentMedia.id }),
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
this.showToast("Session Expired", "Please login again", "error");
|
||||
this.handleLogout();
|
||||
// Redirect to login page after logout
|
||||
setTimeout(() => {
|
||||
// Trigger a page reload to update the UI based on state
|
||||
location.reload();
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.state.statusMessage = `${action === 'approve' ? 'Approved' : 'Rejected'} successfully!`;
|
||||
|
||||
// Show success message briefly, then move to the next item
|
||||
setTimeout(() => {
|
||||
// Remove the current item from the queue
|
||||
this.state.mediaQueue.shift();
|
||||
|
||||
// Update current media
|
||||
if (this.state.mediaQueue.length > 0) {
|
||||
this.state.currentMedia = this.state.mediaQueue[0];
|
||||
// Fetch next item to keep queue full
|
||||
this.fetchNextMedia();
|
||||
} else {
|
||||
this.state.currentMedia = null;
|
||||
}
|
||||
|
||||
this.state.statusMessage = '';
|
||||
this.state.isProcessing = false;
|
||||
}, 1000);
|
||||
} else {
|
||||
this.showToast("Error", data.message || `Failed to ${action} media`, "error");
|
||||
this.state.isProcessing = false;
|
||||
this.state.statusMessage = '';
|
||||
}
|
||||
} catch (error) {
|
||||
this.showToast("Error", "Network error. Please try again.", "error");
|
||||
this.state.isProcessing = false;
|
||||
this.state.statusMessage = '';
|
||||
}
|
||||
},
|
||||
|
||||
handleImageError(e) {
|
||||
const imgEl = e.target;
|
||||
if (!imgEl.dataset.fallbackTried) {
|
||||
imgEl.dataset.fallbackTried = 'true';
|
||||
imgEl.src = this.state.currentMedia.remote_url;
|
||||
} else {
|
||||
// Both preview AND remote have failed - drop it
|
||||
this.state.mediaQueue = this.state.mediaQueue.filter(
|
||||
item => item.id !== this.state.currentMedia.id
|
||||
);
|
||||
|
||||
// Update current media
|
||||
if (this.state.mediaQueue.length > 0) {
|
||||
this.state.currentMedia = this.state.mediaQueue[0];
|
||||
} else {
|
||||
this.state.currentMedia = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
showToast(title, message, variant = 'default') {
|
||||
// Simple toast notification
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 p-4 rounded-md shadow-lg z-50 max-w-md ${
|
||||
variant === 'error' ? 'bg-red-100 border border-red-300 text-red-700' : 'bg-green-100 border border-green-300 text-green-700'
|
||||
}`;
|
||||
toast.innerHTML = `
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<div class="font-bold">${title}</div>
|
||||
<div>${message}</div>
|
||||
</div>
|
||||
<button class="ml-4 text-gray-600 hover:text-gray-900" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Auto remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user