Modernize admin UI with React and update templates

This commit is contained in:
2025-05-24 10:49:30 +03:30
parent d35a93e4bc
commit 0f96344543
9 changed files with 153 additions and 250 deletions

1
go.mod
View File

@@ -11,6 +11,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/cors v1.7.5 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-contrib/static v1.1.5 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect

2
go.sum
View File

@@ -17,6 +17,8 @@ github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYM
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4=
github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
@@ -19,27 +20,31 @@ func SetupRouter() *gin.Engine {
AllowCredentials: true,
}))
r.LoadHTMLGlob("internal/web/templates/**/*")
r.LoadHTMLGlob("internal/web/templates/home/*")
auth.InitJwtTokenGenerator() // Must be befor initializing admin handler, otherwise 'panic: runtime error: invalid memory address or nil pointer dereference'
handlers.InitAdminDashboardHandler()
handlers.InitApiEndpointHandler()
handlers.InitEmbedCardHandler()
// Main page
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "home/index.html", nil)
})
// Embed card
r.GET("/embed", handlers.EmbedCardHandlerInstance.GetEmbedCard)
admin := r.Group("/admin")
// My man, this is done way more efficient and fast in .NET, specially the authentication part
admin.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "admin/index.html", nil)
})
admin.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "admin/login.html", nil)
})
// admin.GET("/", func(c *gin.Context) {
// c.HTML(http.StatusOK, "admin/index.html", nil)
// })
// admin.GET("/login", func(c *gin.Context) {
// c.HTML(http.StatusOK, "admin/index.html", nil)
// })
r.Use(static.Serve("/admin", static.LocalFile("internal/web/templates/admin", true)))
adminApi := admin.Group("/api")
adminApi.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
adminApi.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -1,156 +1,28 @@
{{ define "admin/index.html" }}
<!DOCTYPE html>
<html>
<head>
<title>Media Approval</title>
<style>
body {
font-family: sans-serif;
background-color: #f9f9f9;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 300px;
position: relative;
}
.card img {
max-width: 100%;
border-radius: 4px;
margin-bottom: 15px;
}
button {
margin: 5px;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
color: white;
}
.approve { background-color: #4CAF50; }
.reject { background-color: #f44336; }
.message {
position: absolute;
top: -25px;
left: 0;
right: 0;
text-align: center;
font-size: 14px;
color: #4CAF50;
display: none;
}
</style>
</head>
<body>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Dashboard</title>
<meta name="description" content="Admin Dashboard for Media Review" />
<meta name="author" content="Mahdium" />
<div class="card" id="media-card">
<div class="message" id="status-msg"></div>
<img id="preview" src="" alt="Media Preview" onerror="loadRemote()" />
<div>
<button class="approve" onclick="sendAction('approve')">Approve</button>
<button class="reject" onclick="sendAction('reject')">Reject</button>
</div>
</div>
<meta property="og:title" content="Admin Dashboard" />
<meta property="og:description" content="Admin Dashboard for Media Review" />
<meta property="og:type" content="website" />
<script>
let currentMedia = null;
let nextMedia = null;
let preloadedImage = new Image();
window.onload = async () => {
const token = getToken();
if (!token) {
alert("No token found. Please login first.");
window.location.href = "login";
return;
}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@mahdium86" />
currentMedia = await fetchMedia(token);
showMedia(currentMedia);
preloadNextMedia(token);
};
<script type="module" crossorigin src="/admin/assets/index-CWPfp0-e.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-DShmOgsI.css">
</head>
function getToken() {
const match = document.cookie.match(/(^| )token=([^;]+)/);
return match ? match[2] : null;
}
function showMedia(media) {
const preview = document.getElementById('preview');
preview.src = media.preview_url;
}
function loadRemote() {
if (currentMedia?.remote_url) {
document.getElementById('preview').src = currentMedia.remote_url;
}
}
async function sendAction(action) {
if (!currentMedia?.id) return;
const token = getToken();
const endpoint = `api/${action}`;
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ mediaId: currentMedia.id })
});
showMessage(action.charAt(0).toUpperCase() + action.slice(1) + " successful");
// Show next media
if (nextMedia) {
currentMedia = nextMedia;
showMedia(currentMedia);
preloadNextMedia(token);
} else {
document.getElementById('media-card').innerHTML = 'No more media.';
}
}
function showMessage(text) {
const msgDiv = document.getElementById('status-msg');
msgDiv.innerText = text;
msgDiv.style.display = 'block';
setTimeout(() => {
msgDiv.style.display = 'none';
}, 500);
}
async function fetchMedia(token) {
const res = await fetch('api/getmedia', {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + token }
});
return await res.json();
}
async function preloadNextMedia(token) {
try {
nextMedia = await fetchMedia(token);
preloadedImage.src = nextMedia.preview_url;
preloadedImage.onerror = () => {
preloadedImage.src = nextMedia.remote_url;
};
} catch (err) {
console.error("Failed to preload next media:", err);
nextMedia = null;
}
}
</script>
</body>
<body>
<div id="root"></div>
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
<script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
</body>
</html>
{{ end }}

View File

@@ -1,92 +0,0 @@
{{ define "admin/login.html" }}
<!DOCTYPE html>
<html>
<head>
<title>Simple Login Page</title>
<style>
body {
font-family: sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
input[type="password"] {
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #3e8e41;
}
#message {
margin-top: 10px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="login-container">
<h2>Login</h2>
<input type="password" id="password" placeholder="Password"><br>
<button onclick="login()">Login</button>
<div id="message"></div>
</div>
<script>
function login() {
const password = document.getElementById('password').value;
fetch('api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ "Password": password })
})
.then(response => response.json())
.then(data => {
if (data.message === "Login successful") {
document.getElementById('message').innerText = data.message;
saveToken(data.token);
setTimeout(() => {
window.location.href = "/admin/";
}, 1000);
} else {
document.getElementById('message').innerText = "Login failed.";
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('message').innerText = "An error occurred.";
});
}
function saveToken(token) {
document.cookie = "token=" + token + "; path=/";
}
</script>
</body>
</html>
{{ end }}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB