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 +` + + + + + ` + file.spice_level +` + + + + `); + 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 - - - - - 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 + + +

+
+ + + + + + + + + + + +
IDImageSpice 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 + + + + images + + + +