Add web templates and handlers for home, embed card, and admin pages
This commit is contained in:
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"CatsOfMastodonBotGo/internal/auth"
|
"CatsOfMastodonBotGo/internal/auth"
|
||||||
"CatsOfMastodonBotGo/internal/web/handlers"
|
"CatsOfMastodonBotGo/internal/web/handlers"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -18,17 +19,32 @@ func SetupRouter() *gin.Engine {
|
|||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
r.LoadHTMLGlob("internal/web/templates/**/*")
|
||||||
|
|
||||||
auth.InitJwtTokenGenerator() // Must be befor initializing admin handler, otherwise 'panic: runtime error: invalid memory address or nil pointer dereference'
|
auth.InitJwtTokenGenerator() // Must be befor initializing admin handler, otherwise 'panic: runtime error: invalid memory address or nil pointer dereference'
|
||||||
handlers.InitAdminDashboardHandler()
|
handlers.InitAdminDashboardHandler()
|
||||||
handlers.InitApiEndpointHandler()
|
handlers.InitApiEndpointHandler()
|
||||||
|
handlers.InitEmbedCardHandler()
|
||||||
|
|
||||||
|
r.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "home/index.html", nil)
|
||||||
|
})
|
||||||
|
r.GET("/embed", handlers.EmbedCardHandlerInstance.GetEmbedCard)
|
||||||
|
|
||||||
admin := r.Group("/admin")
|
admin := r.Group("/admin")
|
||||||
|
|
||||||
// My man, this is done way more efficient and fast in .NET, specially the authentication part
|
// My man, this is done way more efficient and fast in .NET, specially the authentication part
|
||||||
admin.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
|
admin.GET("/", func(c *gin.Context) {
|
||||||
admin.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia)
|
c.HTML(http.StatusOK, "admin/index.html", nil)
|
||||||
admin.POST("/approve", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.ApproveMedia)
|
})
|
||||||
admin.POST("/reject", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.RejectMedia)
|
admin.GET("/login", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "admin/login.html", nil)
|
||||||
|
})
|
||||||
|
adminApi := admin.Group("/api")
|
||||||
|
adminApi.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
|
||||||
|
adminApi.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia)
|
||||||
|
adminApi.POST("/approve", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.ApproveMedia)
|
||||||
|
adminApi.POST("/reject", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.RejectMedia)
|
||||||
|
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
|
|
||||||
|
@@ -64,7 +64,6 @@ func (ps *PostService) GetExistingAccountIds() []string {
|
|||||||
return existingAccountIds
|
return existingAccountIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (*PostService) GetNewPosts(existingPostIds []string, posts []domain.Post) []domain.Post {
|
func (*PostService) GetNewPosts(existingPostIds []string, posts []domain.Post) []domain.Post {
|
||||||
var newPosts []domain.Post = nil
|
var newPosts []domain.Post = nil
|
||||||
for _, post := range posts {
|
for _, post := range posts {
|
||||||
@@ -108,7 +107,8 @@ func (ps *PostService) GetRandomPost() domain.Post {
|
|||||||
var post domain.Post
|
var post domain.Post
|
||||||
ps.db.
|
ps.db.
|
||||||
Preload("Account").
|
Preload("Account").
|
||||||
Preload("Attachments", "approved = ?", true).
|
Preload("Attachments", "Approved = ?", true).
|
||||||
|
Where("exists (select 1 from media_attachments where post_id = posts.id and approved = ?)", true).
|
||||||
Order("RANDOM()").
|
Order("RANDOM()").
|
||||||
First(&post)
|
First(&post)
|
||||||
if len(post.Attachments) > 0 {
|
if len(post.Attachments) > 0 {
|
||||||
@@ -140,7 +140,6 @@ func (ps *PostService) GetMedia() domain.MediaAttachment {
|
|||||||
return media
|
return media
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func arrayContains(arr []string, str string) bool {
|
func arrayContains(arr []string, str string) bool {
|
||||||
for _, a := range arr {
|
for _, a := range arr {
|
||||||
if a == str {
|
if a == str {
|
||||||
|
27
internal/web/handlers/embedCard.go
Normal file
27
internal/web/handlers/embedCard.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"CatsOfMastodonBotGo/internal/services"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmbedCardHandler struct {
|
||||||
|
PostService services.PostService
|
||||||
|
}
|
||||||
|
|
||||||
|
var EmbedCardHandlerInstance *EmbedCardHandler
|
||||||
|
|
||||||
|
func InitEmbedCardHandler() {
|
||||||
|
EmbedCardHandlerInstance = &EmbedCardHandler{
|
||||||
|
PostService: *services.PostServiceInstance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *EmbedCardHandler) GetEmbedCard(c *gin.Context) {
|
||||||
|
post := ps.PostService.GetRandomPost()
|
||||||
|
c.HTML(200, "home/embed.html", gin.H{
|
||||||
|
"postUrl": post.Url,
|
||||||
|
"imageUrl": post.Attachments[0].PreviewUrl,
|
||||||
|
})
|
||||||
|
}
|
156
internal/web/templates/admin/index.html
Normal file
156
internal/web/templates/admin/index.html
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
{{ 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMedia = await fetchMedia(token);
|
||||||
|
showMedia(currentMedia);
|
||||||
|
preloadNextMedia(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = `admin/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('admin/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>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
92
internal/web/templates/admin/login.html
Normal file
92
internal/web/templates/admin/login.html
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{{ 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('admin/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 = "index.html";
|
||||||
|
}, 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 }}
|
62
internal/web/templates/home/embed.html
Normal file
62
internal/web/templates/home/embed.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{{ define "home/embed.html" }}
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Cat of the Day</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Atma:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #161616;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 20px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
font-family: "Atma", system-ui;
|
||||||
|
|
||||||
|
}
|
||||||
|
.cat-image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
object-fit: cover;
|
||||||
|
max-height: auto;
|
||||||
|
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #9e44ae;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: none;
|
||||||
|
padding: 2px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease-in-out;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #6d0e7e;
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card" style="margin: 0 auto;">
|
||||||
|
<img class="cat-image" id="catImage" src="{{ .imageUrl }}" alt="Cat Photo">
|
||||||
|
<a class="button" id="mastodonLink" href="{{ .postUrl }}" target="_blank" style="display: block;">View on Mastodon</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
298
internal/web/templates/home/index.html
Normal file
298
internal/web/templates/home/index.html
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
{{ define "home/index.html" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Cats of Mastodon! - Mahdium</title><style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Atma:wght@300;400;500;600;700&display=swap');
|
||||||
|
body {
|
||||||
|
font-family: "Atma", system-ui;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-title {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message span {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container {
|
||||||
|
background-color: #282828;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 20px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title-inner {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
color: #ffb347;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-text {
|
||||||
|
color: #bbdefb;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
object-fit: cover;
|
||||||
|
max-height: 500px;
|
||||||
|
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image.loading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-loading-spinner {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #6364ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image.loading + .image-loading-spinner {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-button,
|
||||||
|
.neutral-button {
|
||||||
|
background-color: #404040;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease-in-out;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-button {
|
||||||
|
background-color: #6364ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastodon-button:hover {
|
||||||
|
background-color: #5253e0;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neutral-button:hover {
|
||||||
|
background-color: #505050;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: none;
|
||||||
|
border: 6px solid #f3f3f3;
|
||||||
|
border-top: 6px solid #6364ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container.loading .loading-spinner {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-container.loading .post-image,
|
||||||
|
.post-container.loading .button-container,
|
||||||
|
.post-container.loading .post-title-inner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #909090;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2 class="mastodon-title">Cats of Mastodon!</h2>
|
||||||
|
<p class="welcome-message">Welcome to Daily Catventures! </br> Get your daily dose of purr-fectly adorable feline fun! <span>✨</span><br>Posts gathered across Mastodon 🤝</p>
|
||||||
|
|
||||||
|
<div class="post-container">
|
||||||
|
<div class="post-content" style="display: none;">
|
||||||
|
<div class="post-title-inner">
|
||||||
|
<span class="user-name"></span><span class="cat-text">'s cat!</span>
|
||||||
|
</div>
|
||||||
|
<div class="image-container">
|
||||||
|
<img class="post-image" src="" alt="Cat Photo">
|
||||||
|
<div class="image-loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="button-container">
|
||||||
|
<a class="mastodon-button" href="" target="_blank">View on Mastodon</a>
|
||||||
|
<button class="neutral-button" onclick="loadNewPost()">Show me another cat!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<span>© 2024 Mahdium</span>
|
||||||
|
<a href="https://mahdium.ir" class="footer-button">mahdium.ir</a>
|
||||||
|
<a href="https://gitlab.com/mahdium/cats-of-mastodon-telegram-bot" class="footer-button">Source Code</a>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const postContainer = document.querySelector('.post-container');
|
||||||
|
const postContent = document.querySelector('.post-content');
|
||||||
|
const userNameSpan = document.querySelector('.post-content .user-name');
|
||||||
|
const postImage = document.querySelector('.post-content .post-image');
|
||||||
|
const mastodonLink = document.querySelector('.post-content .mastodon-button');
|
||||||
|
const imageLoadingSpinner = document.querySelector('.image-container .image-loading-spinner');
|
||||||
|
|
||||||
|
function loadNewPost() {
|
||||||
|
postContainer.classList.add('loading');
|
||||||
|
postContent.style.display = 'none';
|
||||||
|
postImage.classList.add('loading');
|
||||||
|
|
||||||
|
fetch('/api/post/random')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
postContainer.classList.remove('loading');
|
||||||
|
postContent.style.display = 'block';
|
||||||
|
|
||||||
|
if (!data || !data.account || !data.media_attachments || data.media_attachments.length === 0) {
|
||||||
|
console.error("Invalid API response format:", data);
|
||||||
|
alert("Invalid data received from the server.");
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userNameSpan.textContent = data.account.display_name;
|
||||||
|
|
||||||
|
let imageUrl = data.media_attachments[0].remote_url;
|
||||||
|
if (imageUrl) {
|
||||||
|
imageUrl = imageUrl.replace('/original/', '/small/');
|
||||||
|
} else if (data.media_attachments[0].PreviewUrl) {
|
||||||
|
imageUrl = data.media_attachments[0].PreviewUrl;
|
||||||
|
} else {
|
||||||
|
console.warn("No RemoteUrl or PreviewUrl found, using placeholder");
|
||||||
|
postImage.src = "https://s6.uupload.ir/files/a69d5fc9e900cc51_1920_kmnr.png";
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postImage.onload = () => {
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
postImage.onerror = () => {
|
||||||
|
console.error("Error loading image:", imageUrl);
|
||||||
|
postImage.src = "https://s6.uupload.ir/files/a69d5fc9e900cc51_1920_kmnr.png";
|
||||||
|
loadNewPost();
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
};
|
||||||
|
|
||||||
|
postImage.src = imageUrl;
|
||||||
|
mastodonLink.href = data.url;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
postContainer.classList.remove('loading');
|
||||||
|
postImage.classList.remove('loading');
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
alert("Failed to load a new post. Please try again later.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNewPost();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
Reference in New Issue
Block a user