diff --git a/.example.env b/.example.env
new file mode 100644
index 0000000..db8b3a2
--- /dev/null
+++ b/.example.env
@@ -0,0 +1,5 @@
+POSTGRES_USER=
+POSTGRES_PWD=
+POSTGRES_DB=
+POSTGRES_HOST=
+POSTGRES_PORT=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ceeb05b..d021f00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
/tmp
+/uploads/*
+.env
\ No newline at end of file
diff --git a/README.md b/README.md
index 09689c2..a6fead2 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/api/FileHandlers.go b/api/FileHandlers.go
new file mode 100644
index 0000000..accc421
--- /dev/null
+++ b/api/FileHandlers.go
@@ -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)
+}
diff --git a/api/MetadataSaving.go b/api/MetadataSaving.go
new file mode 100644
index 0000000..21fe2e2
--- /dev/null
+++ b/api/MetadataSaving.go
@@ -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
+ }
+ }
+ }
+
+}
diff --git a/api/UploadHandlers.go b/api/UploadHandlers.go
new file mode 100644
index 0000000..1d7b011
--- /dev/null
+++ b/api/UploadHandlers.go
@@ -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, "")
+ 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)
+}
diff --git a/config/env.go b/config/env.go
new file mode 100644
index 0000000..b4da19a
--- /dev/null
+++ b/config/env.go
@@ -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"),
+ },
+ }
+}
diff --git a/db/db.go b/db/db.go
new file mode 100644
index 0000000..8f36e2b
--- /dev/null
+++ b/db/db.go
@@ -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
+}
diff --git a/go.mod b/go.mod
index 9827139..6a62227 100644
--- a/go.mod
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..6b95218
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/main.go b/main.go
index 22c730c..809559b 100644
--- a/main.go
+++ b/main.go
@@ -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)
}
diff --git a/src/FileHandlers.go b/src/FileHandlers.go
deleted file mode 100644
index 382c3a5..0000000
--- a/src/FileHandlers.go
+++ /dev/null
@@ -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)
-}
diff --git a/static/css/admin.css b/static/css/admin.css
new file mode 100644
index 0000000..d087594
--- /dev/null
+++ b/static/css/admin.css
@@ -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;
+}
diff --git a/static/css/global.css b/static/css/global.css
new file mode 100644
index 0000000..ec922ea
--- /dev/null
+++ b/static/css/global.css
@@ -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%;
+}
\ No newline at end of file
diff --git a/static/js/admin.js b/static/js/admin.js
new file mode 100644
index 0000000..c83cf00
--- /dev/null
+++ b/static/js/admin.js
@@ -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" )
+})
diff --git a/static/js/image_upload.js b/static/js/image_upload.js
new file mode 100644
index 0000000..1300d97
--- /dev/null
+++ b/static/js/image_upload.js
@@ -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);
+ }
+ });
+})
\ No newline at end of file
diff --git a/static/js/images.js b/static/js/images.js
new file mode 100644
index 0000000..3c666cd
--- /dev/null
+++ b/static/js/images.js
@@ -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 = $(`
+
+ | `+ i +` |
+
+
+ |
+
+
+
+ |
+
+ `);
+ 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();
+});
\ No newline at end of file
diff --git a/templates/images.html b/templates/images.html
deleted file mode 100644
index 8b0a559..0000000
--- a/templates/images.html
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
- IMAGES
-
-
-
-
-
-
-
-
-
diff --git a/uploads/DSC_0094.jpg b/uploads/DSC_0094.jpg
new file mode 100644
index 0000000..2a56d39
Binary files /dev/null and b/uploads/DSC_0094.jpg differ
diff --git a/website/Admin.go b/website/Admin.go
new file mode 100644
index 0000000..d78da5d
--- /dev/null
+++ b/website/Admin.go
@@ -0,0 +1 @@
+package admin
diff --git a/website/templates/admin.html b/website/templates/admin.html
new file mode 100644
index 0000000..7d45631
--- /dev/null
+++ b/website/templates/admin.html
@@ -0,0 +1,53 @@
+
+
+
+
+ Admin
+
+
+
+
+
+
+
+
+
+
Add New Image
+
+
![Image Preview]()
+
+
+
+
+ Images
+
+
+
+
+
+
+
+ | ID |
+ Image |
+ Spice lvl |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/ascii.html b/website/templates/ascii.html
similarity index 80%
rename from templates/ascii.html
rename to website/templates/ascii.html
index c66ca4e..c7d728d 100644
--- a/templates/ascii.html
+++ b/website/templates/ascii.html
@@ -5,24 +5,7 @@
ASCII
-
+
diff --git a/website/templates/images.html b/website/templates/images.html
new file mode 100644
index 0000000..e1843bc
--- /dev/null
+++ b/website/templates/images.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+ IMAGES
+
+
+
+
+
+
+
+