Added a administration page #1

Open
olived wants to merge 10 commits from olived/Advertisement_Panel:main into main
23 changed files with 606 additions and 141 deletions

5
.example.env Normal file
View File

@@ -0,0 +1,5 @@
POSTGRES_USER=
POSTGRES_PWD=
POSTGRES_DB=
POSTGRES_HOST=
POSTGRES_PORT=

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/tmp
/uploads/*
.env

View File

@@ -27,15 +27,15 @@ $(go env GOPATH)/bin/air -c .air.toml
# Tasks
- [<]  Interface for adding new images
- [x] Interface for adding new images
- [x] ASCII ART support
- [ ] some sort of auth
- [ ] Change image system to database
- [ ] Add support for user inputed webassembly
- [>] Change image system to database
- [ ] Add support for user inputed webassembly
- [ ] ATB integration
- [ ] Show more spicy images after kl 22:00
- [ ] Hide mouse cursor (do this though linux)
- [ ] More images and memes???
- [ ] Show more spicy images after kl 22:00
- [>] Hide mouse cursor (do this though linux)
- [x] More images and memes???
# NB!!!!
Changes in the static directory will not rebuild the project, simple workaround is to just make a small change in main.go and save, for example adding a space

17
api/FileHandlers.go Normal file
View File

@@ -0,0 +1,17 @@
package api
import (
"Advertisement_Panel/db"
"encoding/json"
"net/http"
)
// move FileData and AsciiEntry here if you want, or leave in main.go
func FileHandler(w http.ResponseWriter, r *http.Request) {
var data []db.ImagesEntry
data = db.GetImageMetadata()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}

52
api/MetadataSaving.go Normal file
View File

@@ -0,0 +1,52 @@
package api
import (
"Advertisement_Panel/db"
"encoding/json"
"io"
"log"
"net/http"
)
func SaveMetadataHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
var newMetadata []db.ImagesEntry
log.Println(string(body))
err = json.Unmarshal(body, &newMetadata)
if err != nil {
log.Println("Error unmarshalling JSON:", err)
http.Error(w, "Failed to parse JSON", http.StatusBadRequest)
return
}
var currentMetadata []db.ImagesEntry
currentMetadata = db.GetImageMetadata()
for _, newEntry := range newMetadata {
for _, currentEntry := range currentMetadata {
if newEntry.ID == currentEntry.ID {
if newEntry.SpiceLevel != currentEntry.SpiceLevel {
_, err := db.DB.Exec("UPDATE images SET spice_level = $1 WHERE id = $2", newEntry.SpiceLevel, newEntry.ID)
if err != nil {
http.Error(w, "Failed to update database", http.StatusInternalServerError)
return
}
}
break
}
}
}
}

38
api/UploadHandlers.go Normal file
View File

@@ -0,0 +1,38 @@
package api
import (
"Advertisement_Panel/db"
"fmt"
"io"
"net/http"
"os"
)
func UploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
file, header, err := r.FormFile("image") // "image" is the name of the file input
if err != nil {
http.Error(w, "Error retrieving file", http.StatusBadRequest)
return
}
defer file.Close()
dst, err := os.Create("./uploads/" + header.Filename)
if err != nil {
http.Error(w, "Error creating file on server", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, "Error saving file", http.StatusInternalServerError)
return
}
fmt.Fprint(w, "<script>location.href = '/admin/'</script>")
fmt.Fprintf(w, "Image uploaded successfully: %s", header.Filename)
db.DB.Exec("INSERT INTO images (path, spice_level) VALUES ($1, $2)", "/uploads/"+header.Filename, 0)
}

45
config/env.go Normal file
View File

@@ -0,0 +1,45 @@
package config
import (
"os"
// this will automatically load your .env file:
_ "github.com/joho/godotenv/autoload"
)
type Config struct {
Logs LogConfig
DB PostgresConfig
Port string
}
type LogConfig struct {
Style string
Level string
}
type PostgresConfig struct {
Username string
Password string
Db string
Host string
Port string
}
var Cfg *Config
func LoadConfig() {
Cfg = &Config{
Port: os.Getenv("PORT"),
Logs: LogConfig{
Style: os.Getenv("LOG_STYLE"),
Level: os.Getenv("LOG_LEVEL"),
},
DB: PostgresConfig{
Username: os.Getenv("POSTGRES_USER"),
Password: os.Getenv("POSTGRES_PWD"),
Db: os.Getenv("POSTGRES_DB"),
Host: os.Getenv("POSTGRES_HOST"),
Port: os.Getenv("POSTGRES_PORT"),
},
}
}

58
db/db.go Normal file
View File

@@ -0,0 +1,58 @@
package db
import (
"Advertisement_Panel/config"
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
type ImagesEntry struct {
ID int `json:"id"`
Path string `json:"path"`
SpiceLevel int `json:"spice_level"`
}
var DB *sql.DB
func Init() {
log.Println("Initializing database connection...")
connStr := fmt.Sprintf("host=%s port=%s user=%s "+
"password=%s dbname=%s sslmode=disable",
config.Cfg.DB.Host, config.Cfg.DB.Port, config.Cfg.DB.Username, config.Cfg.DB.Password, config.Cfg.DB.Db)
log.Println(connStr)
var err error
DB, err = sql.Open("postgres", connStr)
if err != nil {
log.Println("Error connecting to the database: ", err)
}
}
func GetImageMetadata() []ImagesEntry {
rows, err := DB.Query("SELECT id, path, spice_level FROM images")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var data []ImagesEntry
for rows.Next() {
var id int
var path string
var spiceLevel int
if err := rows.Scan(&id, &path, &spiceLevel); err != nil {
log.Fatal(err)
}
data = append(data, ImagesEntry{ID: id, Path: path, SpiceLevel: spiceLevel})
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
return data
}

5
go.mod
View File

@@ -1,3 +1,8 @@
module Advertisement_Panel
go 1.24
require (
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9 // indirect
)

8
go.sum Normal file
View File

@@ -0,0 +1,8 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=

35
main.go
View File

@@ -1,23 +1,29 @@
package main
import (
"Advertisement_Panel/src"
"Advertisement_Panel/api"
"Advertisement_Panel/config"
"Advertisement_Panel/db"
"fmt"
"html/template"
"net/http"
)
var templ *template.Template
var templates *template.Template
func main() {
templ, _ = template.ParseGlob("templates/*.html")
templates, _ = template.ParseGlob("website/templates/*.html")
fmt.Print("Now running server!\n")
config.LoadConfig()
db.Init()
// Serves index
http.HandleFunc("/", index_handler)
// Serves json for html to find file names
http.HandleFunc("/files", src.FileHandler)
http.HandleFunc("/files", api.FileHandler)
// Serves ascii page
http.HandleFunc("/ascii", ascii_handler)
@@ -25,14 +31,31 @@ func main() {
// Serves images
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
http.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir("./uploads"))))
// Serves administration page
http.HandleFunc("/admin/", admin_handler)
// Handles image upload
http.HandleFunc("/api/upload", api.UploadHandler)
// Handles metadata saving
http.HandleFunc("/api/save-metadata", api.SaveMetadataHandler)
fmt.Print("Webserver running on http://localhost:8080\n")
// Serves what ever the user is requesting base on above urls
http.ListenAndServe(":8080", nil)
}
func index_handler(w http.ResponseWriter, r *http.Request) {
templ.ExecuteTemplate(w, "images.html", nil)
templates.ExecuteTemplate(w, "images.html", nil)
}
func ascii_handler(w http.ResponseWriter, r *http.Request) {
templ.ExecuteTemplate(w, "ascii.html", nil)
templates.ExecuteTemplate(w, "ascii.html", nil)
}
func admin_handler(w http.ResponseWriter, r *http.Request) {
templates.ExecuteTemplate(w, "admin.html", nil)
}

View File

@@ -1,69 +0,0 @@
package src
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
)
const staticDir = "static"
type FileData struct {
ImageNames []string
SpicyImageNames []string
AsciiFiles []AsciiEntry
}
type AsciiEntry struct {
Name string
FontSize int
}
// move FileData and AsciiEntry here if you want, or leave in main.go
func FileHandler(w http.ResponseWriter, r *http.Request) {
data := FileData{
ImageNames: []string{},
SpicyImageNames: []string{},
AsciiFiles: []AsciiEntry{},
}
dirs, err := os.ReadDir(staticDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, dir := range dirs {
dirName := dir.Name()
if strings.EqualFold(dirName, "images") {
files, _ := os.ReadDir(filepath.Join(staticDir, dirName))
for _, file := range files {
fileName := file.Name()
data.ImageNames = append(data.ImageNames, filepath.Join(staticDir, dirName, fileName))
}
} else if strings.EqualFold(dirName, "spicy") {
files, _ := os.ReadDir(filepath.Join(staticDir, dirName))
for _, file := range files {
fileName := file.Name()
data.SpicyImageNames = append(data.SpicyImageNames, filepath.Join(staticDir, dirName, fileName))
}
} else if strings.EqualFold(dirName, "ascii_art") {
files, _ := os.ReadDir(filepath.Join(staticDir, dirName))
for _, file := range files {
fileName := file.Name()
data.AsciiFiles = append(data.AsciiFiles,
AsciiEntry{Name: filepath.Join(staticDir, dirName, fileName), FontSize: 12},
)
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}

91
static/css/admin.css Normal file
View File

@@ -0,0 +1,91 @@
body {
font-family: Arial, sans-serif;
width: 100vw;
height: 100vh;
background: #101010;
color: #fff;
}
.container {
background: #212121;
padding: 24px 32px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #121212;
}
nav {
padding: 10px 18px;
border-radius: 8px;
width: 100%;
box-shadow: 0 2px 8px rgba(0,0,0,0.08)
}
h2 {
margin-bottom: 20px;
color: #ccc;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input[type="file"] {
margin-bottom: 16px;
}
button[type="submit"] {
background: #007bff;
color: #fff;
border: none;
padding: 10px 18px;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background: #0056b3;
}
.preview {
margin-top: 16px;
max-width: 100%;
max-height: 200px;
display: none;
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-container {
display: none;
padding: 20px;
width: 100%;
}
.tab-container.active {
display: block;
}
.tab-button {
background: #333;
color: #fff;
border: none;
padding: 10px 18px;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin-right: 8px;
}
.tab-button:hover {
background: #555;
}
.tab-button.active {
background: #007bff;
}
.gallery-image {
max-width: 90px;
max-height: 70px;
margin: 10px;
border: 2px solid #444;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.3s;
}

22
static/css/global.css Normal file
View File

@@ -0,0 +1,22 @@
body {
margin: 0;
background: black;
display: flex;
justify-content: center;
align-items: center;
color: white;
height: 100vh;
cursor: none;
}
pre {
white-space: pre;
font-family: monospace;
font-size: 14px;
line-height: 1.2;
}
img {
max-width: 100%;
max-height: 100%;
}

11
static/js/admin.js Normal file
View File

@@ -0,0 +1,11 @@
let tab_buttons = $( ".tab-button" )
let tab_containers = $( ".tab-container" )
tab_buttons.on( "click", function() {
let id = $( this ).attr( "id" )
tab_buttons.removeClass( "active" )
tab_containers.removeClass( "active" )
$( this ).addClass( "active" )
console.log( id )
$( "#tab-" + id ).addClass( "active" )
})

49
static/js/image_upload.js Normal file
View File

@@ -0,0 +1,49 @@
let imageInput = $("#imageInput")
let imagePreview = $("#preview")
let uploadForm = $("#imageForm")
let currentFile = null;
imageInput.on("change", function() {
currentFile = this.files[0];
if (currentFile) {
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
preview.style.display = 'block';
}
reader.readAsDataURL(currentFile);
} else {
preview.style.display = 'none';
}
})
uploadForm.on("submit", function(e) {
e.preventDefault();
if (!currentFile) {
alert("Please select a file first.");
return;
}
console.log(currentFile);
let formData = new FormData(this);
formData.append("image", currentFile);
$.ajax({
url: "/api/upload",
type: "POST",
data: formData,
processData: false,
contentType: false,
success: function(response) {
alert("Image uploaded successfully!");
preview.style.display = 'none';
currentFile = null;
},
error: function(xhr, status, error) {
alert("Error uploading image: " + xhr.responseText);
}
});
})

64
static/js/images.js Normal file
View File

@@ -0,0 +1,64 @@
let galleryTable = $("#imagesGallery")
let refreshButton = $("#refresh-gallery")
let saveButton = $("#save-gallery")
let fileMetaData = []
async function fetchImages() {
await $.ajax({
url: '/files',
type: 'GET',
success: function(response) {
fileMetaData = response;
console.log(fileMetaData);
},
error: function() {
alert('Error fetching images.');
}
});
fileMetaData.forEach((file, i) => {
let newRow = $(`
<tr>
<td>`+ i +`</td>
<td>
<img src="` + file.path + `" class="gallery-image" />
</td>
<td class="spice-slider-td">
<output id="spice-output-` + file.id + `">` + file.spice_level +`</output>
<input type="range" min="0" max="10" value="` + file.spice_level + `" class="spice-slider" id="spice-slider-` + file.id + `" />
</td>
</tr>
`);
galleryTable.append(newRow);
});
fileMetaData.forEach(file => {
$("#spice-slider-" + file.id).on("input", function() {
file.spice_level = Number(this.value);
$("#spice-output-" + file.id).text(this.value);
console.log(fileMetaData);
})
});
}
saveButton.on("click", function() {
$.ajax({
url: '/api/save-metadata',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(fileMetaData),
success: function() {
alert('Metadata saved successfully.');
},
error: function() {
alert('Error saving metadata.');
}
});
})
refreshButton.on("click", function() {
galleryTable.empty();
fetchImages();
});
$( document ).ready(function() {
fetchImages();
});

View File

@@ -1,42 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="/static/images/pvv_logo.png">
<title>IMAGES</title>
<style>
body {
margin: 0;
background: black;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
img {
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<img id="images" src="/static/images/pvv_logo.png" alt="images">
<script>
fetch("/files")
.then(res => res.json())
.then(files => {
const images = files.ImageNames
console.log(images)
let index = 0;
setInterval(() => {
index = (index + 1) % images.length;
document.getElementById("images").src = images[index];
}, 1000);
});
</script>
</body>
</html>

BIN
uploads/DSC_0094.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

1
website/Admin.go Normal file
View File

@@ -0,0 +1 @@
package admin

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin</title>
<link rel="stylesheet" href="/static/css/admin.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body>
<main>
<nav>
<button class="tab-button" id="1">Image Upload</button>
<button class="tab-button" id="2">Images</button>
</nav>
<div class="tab-container active" id="tab-1">
<div class="container" style="max-width: 400px;">
<h2>Add New Image</h2>
<form id="imageForm" enctype="multipart/form-data">
<label for="imageInput">Select Image:</label>
<input type="file" id="imageInput" name="image" accept="image/*" required><br>
<button type="submit" style="width: 100%;">Upload</button>
</form>
<img id="preview" class="preview" alt="Image Preview">
</div>
</div>
<div class="tab-container" id="tab-2">
<h1>
Images
<button id="refresh-gallery">Refresh</button>
<button id="save-gallery">Save</button>
</h1>
<div>
<table id="imagesGallery">
<thead>
<tr>
<th>ID</th>
<th>Image</th>
<th>Spice lvl</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</main>
<script src="/static/js/admin.js"></script>
<script src="/static/js/image_upload.js"></script>
<script src="/static/js/images.js"></script>
</body>
</html>

View File

@@ -5,24 +5,7 @@
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="/static/images/pvv_logo.png">
<title>ASCII</title>
<style>
body {
margin: 0;
background: black;
display: flex;
justify-content: center;
align-items: center;
color: white;
height: 100vh;
}
pre {
white-space: pre;
font-family: monospace;
font-size: 14px;
line-height: 1.2;
}
</style>
<link rel="stylesheet" href="/static/css/global.css">
</head>
<body>

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="/static/images/pvv_logo.png">
<link rel="stylesheet" href="/static/css/global.css">
<title>IMAGES</title>
</head>
<body>
<img id="images" src="/static/images/pvv_logo.png" alt="images">
<script>
function setImage(path) {
document.getElementById("images").src = path;
}
let imageMetadata = [];
fetch("/files")
.then(res => res.json())
.then(files => {
imageMetadata = files
let index = 1;
setInterval(() => {
if(index > imageMetadata.length) index = 0;
if(index == 0) {
setImage("/static/images/pvv_logo.png");
} else {
setImage(imageMetadata[index-1].path);
}
index++;
}, 4000);
if(images.length < 1) {
setImage("/static/images/pvv_logo.png");
return
} else {
setTimeout(() => {
//window.location.reload();
}, images.length*2*1000);
}
});
</script>
</body>
</html>