From d2c73a5a13b5bf332afc052547349939c122ca2b Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Fri, 7 Mar 2025 12:44:13 -0300 Subject: [PATCH] Initial Commit --- README.md | 136 +++++++++++++ config/about.template | 25 +++ go.mod | 13 ++ go.sum | 10 + handlers/about.go | 45 +++++ handlers/accept_cookie.go | 22 +++ handlers/app.go | 70 +++++++ handlers/board.go | 127 ++++++++++++ handlers/boards.go | 67 +++++++ handlers/home.go | 34 ++++ handlers/like.go | 78 ++++++++ handlers/login.go | 62 ++++++ handlers/logout.go | 21 ++ handlers/news.go | 38 ++++ handlers/profile.go | 55 ++++++ handlers/profile_edit.go | 65 ++++++ handlers/signup.go | 62 ++++++ handlers/thread.go | 140 +++++++++++++ handlers/userhome.go | 49 +++++ main.go | 100 ++++++++++ models/board.go | 65 ++++++ models/board_permission.go | 46 +++++ models/like.go | 62 ++++++ models/notification.go | 49 +++++ models/post.go | 45 +++++ models/reaction.go | 44 +++++ models/repost.go | 41 ++++ models/sla.html | 28 +++ models/thread.go | 57 ++++++ models/user.go | 89 +++++++++ static/img/ThreadR.png | Bin 0 -> 95410 bytes static/img/ThreadR.svg | 272 ++++++++++++++++++++++++++ static/img/ThreadR_Home.svg | 249 +++++++++++++++++++++++ static/img/favicon-32x32.png | Bin 0 -> 752 bytes static/img/threadR.png | Bin 0 -> 1338 bytes static/style.css | 186 ++++++++++++++++++ templates/base.html | 16 ++ templates/pages/about.html | 21 ++ templates/pages/board.html | 42 ++++ templates/pages/boards.html | 36 ++++ templates/pages/home.html | 21 ++ templates/pages/login.html | 30 +++ templates/pages/news.html | 25 +++ templates/pages/profile.html | 30 +++ templates/pages/profile_edit.html | 29 +++ templates/pages/signup.html | 30 +++ templates/pages/thread.html | 60 ++++++ templates/pages/userhome.html | 21 ++ templates/partials/cookie_banner.html | 7 + templates/partials/navbar.html | 17 ++ 50 files changed, 2837 insertions(+) create mode 100644 README.md create mode 100644 config/about.template create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers/about.go create mode 100644 handlers/accept_cookie.go create mode 100644 handlers/app.go create mode 100644 handlers/board.go create mode 100644 handlers/boards.go create mode 100644 handlers/home.go create mode 100644 handlers/like.go create mode 100644 handlers/login.go create mode 100644 handlers/logout.go create mode 100644 handlers/news.go create mode 100644 handlers/profile.go create mode 100644 handlers/profile_edit.go create mode 100644 handlers/signup.go create mode 100644 handlers/thread.go create mode 100644 handlers/userhome.go create mode 100644 main.go create mode 100644 models/board.go create mode 100644 models/board_permission.go create mode 100644 models/like.go create mode 100644 models/notification.go create mode 100644 models/post.go create mode 100644 models/reaction.go create mode 100644 models/repost.go create mode 100644 models/sla.html create mode 100644 models/thread.go create mode 100644 models/user.go create mode 100644 static/img/ThreadR.png create mode 100644 static/img/ThreadR.svg create mode 100644 static/img/ThreadR_Home.svg create mode 100644 static/img/favicon-32x32.png create mode 100644 static/img/threadR.png create mode 100644 static/style.css create mode 100644 templates/base.html create mode 100644 templates/pages/about.html create mode 100644 templates/pages/board.html create mode 100644 templates/pages/boards.html create mode 100644 templates/pages/home.html create mode 100644 templates/pages/login.html create mode 100644 templates/pages/news.html create mode 100644 templates/pages/profile.html create mode 100644 templates/pages/profile_edit.html create mode 100644 templates/pages/signup.html create mode 100644 templates/pages/thread.html create mode 100644 templates/pages/userhome.html create mode 100644 templates/partials/cookie_banner.html create mode 100644 templates/partials/navbar.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..2842a38 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Welcome to ThreadR Rewritten +This is the source code for the ThreadR Forum Engine (rewritten in go). + +The project originated as a school project with the goal of developing a mix between a forum engine and a social media platform. When school was over, we left the project up for some time with the general intention to continue working on it until I took it down after an extended period of inactivity to host my own website on my server. + +Now, that it is being revived, the original scope of the project doesn’t really make sense anymore (at least to me) so it needs to shift slightly. Below is a list of goals that I would like to see achieved, feel free to discuss this in the issues or commit comments. + +- [ ] come back online (see issue #2) +- [ ] go FOSS (make the source code publicly available under a FOSS license (see issue #5)) +- [ ] make the code portable so everyone can set up their own instance +- [ ] get generic forum functionality going (sign-up, creation of boards, creation of threads within boards, messages, profiles) + +Once these two are given, here are some additional goals both from the original scope of the project as well as my own ideas. Input is welcome. + +- [ ] anonymous posts (users can choose to post anonymously, registered users will have a unique name per thread that stays the same so users can tell each other apart) +- [ ] subscribing to threads +- [ ] "split thread here" feature (kinda like on Reddit when multiple ppl answer to one person) +- [ ] automatic loading of new messages in threads (opt-out in settings) +- [ ] notifications for new messages in subscribed threads (opt-out in settings) +- [ ] question threads with an "accept answer" feature, threads can be marked as question threads on creation +- [ ] like/dislike feature but in better (as in more limited in functionality and more nuanced, kinda like on StackExchange but with two types of likes/dislikes and without showing an actual number) + +\- BodgeMaster + +UPDATE: The ThreadR Forum Engine is now technically host-independent. By default, it still contains the configuration for our local instance but all host-dependent stup information is configurable now. It is still heavily WIP. + +# Installation +First of all, keep in mind that the ThreadR Forum Engine is still in early development and things are subject to change. + +For now, the only way to set up an instance is doing it the manual way; automatic setup will be added in the future. + +This setup guide is assuming that you are on a UNIX-like system and have the following already installed and set up properly: +- Apache with PHP (will most likely also work on other web servers) +- MySQL or MariaDB +- Python 3 +- Bash + +Installation: + +- To install the ThreadR Forum Engine, clone this repository into a directory that the web server has access to but that it outside of any web root. +- Add a database to your MySQL/MariaDB server that contains the tables shown below. +- Create a MySQL/MariaDB user for ThreadR and grant usage privileges for the tables to it. +- Symlink the directory `build/` to your desired location on the web root. ThreadR does not support being linked directly to the webroot. +- adjust the files in `config/` to your setup +- run ./deployment-script.sh to apply configuration +- Optionally symlink `build/redirect_home.html` to all places that you want to redirect to ThreadR. + +Database tables: +- boards + - `id` (int, primary key, auto increment) + - `name` (varchar) + - `user_friendly_name` (varchar) + - `private` (boolean or tinyint(1)) + - `public_visible` (boolean or tinyint(1)) +- posts + - `id` (int, primary key, auto increment) + - `board_id` (int) + - `user_id` (int) + - `post_time` (timestamp, default current_timestamp()) + - `edit_time` (timestamp, may be null, default null, on update current_timestamp()) + - `content` (text, may be null, default null) + - `attachment_hash` (bigint(20), may be null, default null) + - `attachment_name` (varchar, may be null, default null) + - `title` (varchar) + - `reply_to` (int, default -1) +- profiles (do we even use this?) + - `id` (smallint (why? this makes no sense whatsoever), primary key, index (why? probably wanted to do unique)) + - `email` (varchar, index (I think that’s supposed to be unique?)) + - `display_name` (varchar) + - `status` (varchar) + - `about` (very long varchar) + - `website` (varchar) +- users + - `id` (smallint (again, this makes no sense), primary key) + - `name` (varchar, index (again, that’s probably supposed to be unique)) + - `authentication_string` (varchar(128)) + - `authentication_salt` (varchar) + - `authentication_algorithm` (varchar) + - `time_created` (timestamp, default current_timestamp()) + - `time_altered` (timestamp, default current_timestamp(), on update current_timestamp()) + - `verified` (boolean or tinyint(1), default 0) + +# Git based automatic web deployment system +This repository will be automagically pulled by the web server each time something is pushed by a user. + +Dear Developers, Please use pushes sparingly because it takes a while for the server to replace all code variables. + +What this thing does basically equates to: +``` +ssh @ +cd /var/www/git +sudo -u www-data -s +rm -rf ./web-deployment +git clone +cd web-deployment +./deployment-script +exit +logout +``` +TBD: Remove this section when the ThreadR project moves to its final home and this repository is only used for our local setup. + +## Symlinks +The following files and directories are linked to areas where they can be accessed by the web server: + * `build/` → `threadr.lostcave.ddnss.de/` (all files acessible by the web server, READMEs get deleted on deployment) + +# Individual documentation for each file +### [[DIR] src](./src) +This folder contains all the files that are parts of ThreadR directly +### [[DIR] build](./build) +Placeholder folder to link against, will be deleted and recreated by the deployment script, contains the a working instance of ThreadR after successful execution of the deployment script +### [[DIR] config](./config) +A place to store the configuation for a specific ThreadR instance (contains official instance config for now, will be moved elsewhere eventually) +### [[DIR] macros](./macros) +files for use with variable_grabbler.py +### [deployment_script.sh](./deployment_script.sh) +This script is executed each time (or most of the time) the repository gets pushed. +It contains the commands to execute the code variable replcement system and some other useful tasks. +Its working directory is the root of the git repository. +### [LICENSE.md](./LICENSE.md) +A copy of the Apache 2.0 license, the license this project is under +### [NOTICE](./NOTICE) +Copyright notice in plain text format +### [README.md](./README.md) +this file +### [variable_grabbler.py](./variable_grabbler.py) +Custom macro processor, takes two arguments: macro declaration file and the file to be processed + +Macros in code are strings of capitalized characters prefixed and suffixed with %. + +Macro definition format: JSON +"":"" → direct replacement +"":["file",""] → insert file +"":["exec",""] → run command and insert its output from stdout + +~~NOTICE: This file (or rather a more up-to-date version of it) will be moved to a new repository containing the deployment system.~~ +I haven’t exactly figured out how to handle this in the future. It is absolutely necessary to deploy a ThreadR instance because it is used to configure ThreadR so we need a copy of it here. diff --git a/config/about.template b/config/about.template new file mode 100644 index 0000000..ac6a59b --- /dev/null +++ b/config/about.template @@ -0,0 +1,25 @@ +

+ Hello there! This is the official ThreadR instance provided by the ThreadR development team. +

+

+ What is ThreadR? +

+

+ ThreadR is a free and open-source forum engine. That means you can download + it and host an instance of ThreadR on your own web server to run your own forum. +

+

+ The project originated as a school project in 2019 with the goal of building + a forum. When we finished school, the project was abandoned and eventually taken down. + A year later, we decided to revive it and started working on it again. Now that school + is over and we don't necessarily have a a reason to run our own forum anymore, + we shifted the project goal to building a FOSS forum engine. +

+

+ Who are we? +

+

+ We are a small group of (hobby) developers working on ThreadR in our free time. + To get in touch, ... uhh ... There will be a way once ThreadR is fully functional. + For now, you can find us on Discord: discord.gg/r3w3zSkEUE +

\ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..74365a9 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module threadr + +go 1.24.0 + +require ( + github.com/go-sql-driver/mysql v1.9.0 + github.com/gorilla/sessions v1.4.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2b8450f --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +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.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= diff --git a/handlers/about.go b/handlers/about.go new file mode 100644 index 0000000..52875d8 --- /dev/null +++ b/handlers/about.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "io/ioutil" + "log" + "net/http" + "github.com/gorilla/sessions" + "html/template" +) + +func AboutHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + loggedIn := session.Values["user_id"] != nil + + aboutContent, err := ioutil.ReadFile("config/about.template") + if err != nil { + log.Printf("Error reading about.template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := struct { + PageData + AboutContent template.HTML + }{ + PageData: PageData{ + Title: "ThreadR - About", + Navbar: "about", + LoggedIn: loggedIn, + ShowCookieBanner: true, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + AboutContent: template.HTML(aboutContent), + } + + if err := app.Tmpl.ExecuteTemplate(w, "about", data); err != nil { + log.Printf("Error executing template in AboutHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/accept_cookie.go b/handlers/accept_cookie.go new file mode 100644 index 0000000..3947f5f --- /dev/null +++ b/handlers/accept_cookie.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "net/http" + "time" +) + +func AcceptCookieHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{ + Name: "threadr_cookie_banner", + Value: "accepted", + Path: "/", + Expires: time.Now().Add(30 * 24 * time.Hour), + }) + from := r.URL.Query().Get("from") + if from == "" { + from = app.Config.ThreadrDir + "/" + } + http.Redirect(w, r, from, http.StatusFound) + } +} \ No newline at end of file diff --git a/handlers/app.go b/handlers/app.go new file mode 100644 index 0000000..9f4773f --- /dev/null +++ b/handlers/app.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "context" + "database/sql" + "html/template" + "net/http" + "github.com/gorilla/sessions" +) + +type PageData struct { + Title string + Navbar string + LoggedIn bool + ShowCookieBanner bool + BasePath string + StaticPath string + CurrentURL string +} + +type Config struct { + DomainName string `json:"domain_name"` + ThreadrDir string `json:"threadr_dir"` + DBUsername string `json:"db_username"` + DBPassword string `json:"db_password"` + DBDatabase string `json:"db_database"` + DBServerHost string `json:"db_svr_host"` +} + +type App struct { + DB *sql.DB + Store *sessions.CookieStore + Config *Config + Tmpl *template.Template +} + +func (app *App) SessionMW(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session, err := app.Store.Get(r, "session-name") + if err != nil { + session = sessions.NewSession(app.Store, "session-name") + } + if _, ok := session.Values["user_id"].(int); ok { + if session.Values["user_ip"] != r.RemoteAddr || session.Values["user_agent"] != r.UserAgent() { + session.Values = make(map[interface{}]interface{}) + session.Options.MaxAge = -1 + session.Save(r, w) + http.Redirect(w, r, app.Config.ThreadrDir+"/login/?error=session", http.StatusFound) + return + } + ctx := context.WithValue(r.Context(), "session", session) + r = r.WithContext(ctx) + } else { + ctx := context.WithValue(r.Context(), "session", session) + r = r.WithContext(ctx) + } + next(w, r) + } +} + +func (app *App) RequireLoginMW(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + if _, ok := session.Values["user_id"].(int); !ok { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/?error=session", http.StatusFound) + return + } + next(w, r) + } +} \ No newline at end of file diff --git a/handlers/board.go b/handlers/board.go new file mode 100644 index 0000000..ee9c958 --- /dev/null +++ b/handlers/board.go @@ -0,0 +1,127 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + "threadr/models" + "github.com/gorilla/sessions" +) + +func BoardHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + loggedIn := session.Values["user_id"] != nil + userID, _ := session.Values["user_id"].(int) + + boardIDStr := r.URL.Query().Get("id") + boardID, err := strconv.Atoi(boardIDStr) + if err != nil { + http.Error(w, "Invalid board ID", http.StatusBadRequest) + return + } + + board, err := models.GetBoardByID(app.DB, boardID) + if err != nil { + log.Printf("Error fetching board: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if board == nil { + http.Error(w, "Board not found", http.StatusNotFound) + return + } + + if board.Private { + if !loggedIn { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } + hasPerm, err := models.HasBoardPermission(app.DB, userID, boardID, models.PermViewBoard) + if err != nil { + log.Printf("Error checking permission: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if !hasPerm { + http.Error(w, "You do not have permission to view this board", http.StatusForbidden) + return + } + } + + if r.Method == http.MethodPost && loggedIn { + action := r.URL.Query().Get("action") + if action == "create_thread" { + title := r.FormValue("title") + threadType := r.FormValue("type") + if title == "" || (threadType != "classic" && threadType != "chat" && threadType != "question") { + http.Error(w, "Invalid input", http.StatusBadRequest) + return + } + if board.Private { + hasPerm, err := models.HasBoardPermission(app.DB, userID, boardID, models.PermPostInBoard) + if err != nil { + log.Printf("Error checking permission: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if !hasPerm { + http.Error(w, "You do not have permission to post in this board", http.StatusForbidden) + return + } + } + thread := models.Thread{ + BoardID: boardID, + Title: title, + Type: threadType, + CreatedByUserID: userID, + } + err = models.CreateThread(app.DB, thread) + if err != nil { + log.Printf("Error creating thread: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + var threadID int + err = app.DB.QueryRow("SELECT LAST_INSERT_ID()").Scan(&threadID) + if err != nil { + log.Printf("Error getting last insert id: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + http.Redirect(w, r, app.Config.ThreadrDir+"/thread/?id="+strconv.Itoa(threadID), http.StatusFound) + return + } + } + + threads, err := models.GetThreadsByBoardID(app.DB, boardID) + if err != nil { + log.Printf("Error fetching threads: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := struct { + PageData + Board models.Board + Threads []models.Thread + }{ + PageData: PageData{ + Title: "ThreadR - " + board.Name, + Navbar: "boards", + LoggedIn: loggedIn, + ShowCookieBanner: true, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + Board: *board, + Threads: threads, + } + if err := app.Tmpl.ExecuteTemplate(w, "board", data); err != nil { + log.Printf("Error executing template in BoardHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/boards.go b/handlers/boards.go new file mode 100644 index 0000000..78ac299 --- /dev/null +++ b/handlers/boards.go @@ -0,0 +1,67 @@ +package handlers + +import ( + "log" + "net/http" + "github.com/gorilla/sessions" + "threadr/models" +) + +func BoardsHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + loggedIn := session.Values["user_id"] != nil + userID, _ := session.Values["user_id"].(int) + + publicBoards, err := models.GetAllBoards(app.DB, false) + if err != nil { + log.Printf("Error fetching public boards: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + var privateBoards []models.Board + if loggedIn { + privateBoards, err = models.GetAllBoards(app.DB, true) + if err != nil { + log.Printf("Error fetching private boards: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + var accessiblePrivateBoards []models.Board + for _, board := range privateBoards { + hasPerm, err := models.HasBoardPermission(app.DB, userID, board.ID, models.PermViewBoard) + if err != nil { + log.Printf("Error checking permission: %v", err) + continue + } + if hasPerm { + accessiblePrivateBoards = append(accessiblePrivateBoards, board) + } + } + privateBoards = accessiblePrivateBoards + } + + data := struct { + PageData + PublicBoards []models.Board + PrivateBoards []models.Board + }{ + PageData: PageData{ + Title: "ThreadR - Boards", + Navbar: "boards", + LoggedIn: loggedIn, + ShowCookieBanner: true, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + PublicBoards: publicBoards, + PrivateBoards: privateBoards, + } + if err := app.Tmpl.ExecuteTemplate(w, "boards", data); err != nil { + log.Printf("Error executing template in BoardsHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/home.go b/handlers/home.go new file mode 100644 index 0000000..a9c9714 --- /dev/null +++ b/handlers/home.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "log" + "net/http" + "path/filepath" + "github.com/gorilla/sessions" +) + +func HomeHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + loggedIn := session.Values["user_id"] != nil + cookie, _ := r.Cookie("threadr_cookie_banner") + data := struct { + PageData + }{ + PageData: PageData{ + Title: "ThreadR - Home", + Navbar: "home", + LoggedIn: loggedIn, + ShowCookieBanner: cookie == nil || cookie.Value == "", + BasePath: app.Config.ThreadrDir, + StaticPath: filepath.Join(app.Config.ThreadrDir, "static"), + CurrentURL: r.URL.String(), + }, + } + if err := app.Tmpl.ExecuteTemplate(w, "home", data); err != nil { + log.Printf("Error executing template in HomeHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/like.go b/handlers/like.go new file mode 100644 index 0000000..303e70a --- /dev/null +++ b/handlers/like.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + "threadr/models" + "github.com/gorilla/sessions" +) + +func LikeHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + session := r.Context().Value("session").(*sessions.Session) + userID, ok := session.Values["user_id"].(int) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + postIDStr := r.FormValue("post_id") + postID, err := strconv.Atoi(postIDStr) + if err != nil { + http.Error(w, "Invalid post ID", http.StatusBadRequest) + return + } + + likeType := r.FormValue("type") + if likeType != "like" && likeType != "dislike" { + http.Error(w, "Invalid like type", http.StatusBadRequest) + return + } + + existingLike, err := models.GetLikeByPostAndUser(app.DB, postID, userID) + if err != nil { + log.Printf("Error checking existing like: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + if existingLike != nil { + if existingLike.Type == likeType { + err = models.DeleteLike(app.DB, postID, userID) + if err != nil { + log.Printf("Error deleting like: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } else { + err = models.UpdateLikeType(app.DB, postID, userID, likeType) + if err != nil { + log.Printf("Error updating like: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } + } else { + like := models.Like{ + PostID: postID, + UserID: userID, + Type: likeType, + } + err = models.CreateLike(app.DB, like) + if err != nil { + log.Printf("Error creating like: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } +} \ No newline at end of file diff --git a/handlers/login.go b/handlers/login.go new file mode 100644 index 0000000..7a0e122 --- /dev/null +++ b/handlers/login.go @@ -0,0 +1,62 @@ +package handlers + +import ( + "log" + "net/http" + "threadr/models" + "github.com/gorilla/sessions" +) + +func LoginHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + if r.Method == http.MethodPost { + username := r.FormValue("username") + password := r.FormValue("password") + user, err := models.GetUserByUsername(app.DB, username) + if err != nil { + log.Printf("Error fetching user in LoginHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if user == nil || !models.CheckPassword(password, user.AuthenticationSalt, user.AuthenticationAlgorithm, user.AuthenticationString) { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/?error=invalid", http.StatusFound) + return + } + session.Values["user_id"] = user.ID + session.Values["user_ip"] = r.RemoteAddr + session.Values["user_agent"] = r.UserAgent() + if err := session.Save(r, w); err != nil { + log.Printf("Error saving session: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + http.Redirect(w, r, app.Config.ThreadrDir+"/userhome/", http.StatusFound) + return + } + + data := struct { + PageData + Error string + }{ + PageData: PageData{ + Title: "ThreadR - Login", + Navbar: "login", + LoggedIn: false, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + Error: "", + } + if r.URL.Query().Get("error") == "invalid" { + data.Error = "Invalid username or password" + } + + if err := app.Tmpl.ExecuteTemplate(w, "login", data); err != nil { + log.Printf("Error executing template in LoginHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/logout.go b/handlers/logout.go new file mode 100644 index 0000000..13da3fd --- /dev/null +++ b/handlers/logout.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "log" + "net/http" + "github.com/gorilla/sessions" +) + +func LogoutHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + session.Values = make(map[interface{}]interface{}) + session.Options.MaxAge = -1 + if err := session.Save(r, w); err != nil { + log.Printf("Error saving session in LogoutHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + http.Redirect(w, r, app.Config.ThreadrDir+"/", http.StatusFound) + } +} \ No newline at end of file diff --git a/handlers/news.go b/handlers/news.go new file mode 100644 index 0000000..8bf8699 --- /dev/null +++ b/handlers/news.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "log" + "net/http" + "github.com/gorilla/sessions" +) + +func NewsHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + loggedIn := session.Values["user_id"] != nil + newsItems := []string{ + "2020-02-21 Whole Website updated: Homepage, News, Boards, About, Log In, Userhome, Log Out", + "2020-01-06 First Steps done", + } + data := struct { + PageData + News []string + }{ + PageData: PageData{ + Title: "ThreadR - News", + Navbar: "news", + LoggedIn: loggedIn, + ShowCookieBanner: true, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + News: newsItems, + } + if err := app.Tmpl.ExecuteTemplate(w, "news", data); err != nil { + log.Printf("Error executing template in NewsHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/profile.go b/handlers/profile.go new file mode 100644 index 0000000..5a454ec --- /dev/null +++ b/handlers/profile.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "log" + "net/http" + "threadr/models" + "github.com/gorilla/sessions" +) + +func ProfileHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + userID, ok := session.Values["user_id"].(int) + if !ok { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } + user, err := models.GetUserByID(app.DB, userID) + if err != nil { + log.Printf("Error fetching user in ProfileHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if user == nil { + http.Error(w, "User not found", http.StatusNotFound) + return + } + displayName := user.DisplayName + if displayName == "" { + displayName = user.Username + } + data := struct { + PageData + User models.User + DisplayName string + }{ + PageData: PageData{ + Title: "ThreadR - Profile", + Navbar: "profile", + LoggedIn: true, + ShowCookieBanner: false, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + User: *user, + DisplayName: displayName, + } + if err := app.Tmpl.ExecuteTemplate(w, "profile", data); err != nil { + log.Printf("Error executing template in ProfileHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/profile_edit.go b/handlers/profile_edit.go new file mode 100644 index 0000000..c0d29fc --- /dev/null +++ b/handlers/profile_edit.go @@ -0,0 +1,65 @@ +package handlers + +import ( + "log" + "net/http" + "threadr/models" + "github.com/gorilla/sessions" +) + +func ProfileEditHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + userID, ok := session.Values["user_id"].(int) + if !ok { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } + + if r.Method == http.MethodPost { + displayName := r.FormValue("display_name") + pfpURL := r.FormValue("pfp_url") + bio := r.FormValue("bio") + err := models.UpdateUserProfile(app.DB, userID, displayName, pfpURL, bio) + if err != nil { + log.Printf("Error updating profile: %v", err) + http.Error(w, "Failed to update profile", http.StatusInternalServerError) + return + } + http.Redirect(w, r, app.Config.ThreadrDir+"/profile/", http.StatusFound) + return + } + + user, err := models.GetUserByID(app.DB, userID) + if err != nil { + log.Printf("Error fetching user: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if user == nil { + http.Error(w, "User not found", http.StatusNotFound) + return + } + + data := struct { + PageData + User models.User + }{ + PageData: PageData{ + Title: "ThreadR - Edit Profile", + Navbar: "profile", + LoggedIn: true, + ShowCookieBanner: false, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + User: *user, + } + if err := app.Tmpl.ExecuteTemplate(w, "profile_edit", data); err != nil { + log.Printf("Error executing template in ProfileEditHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/signup.go b/handlers/signup.go new file mode 100644 index 0000000..ee55883 --- /dev/null +++ b/handlers/signup.go @@ -0,0 +1,62 @@ +package handlers + +import ( + "log" + "net/http" + "threadr/models" + "github.com/gorilla/sessions" +) + +func SignupHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + if r.Method == http.MethodPost { + username := r.FormValue("username") + password := r.FormValue("password") + err := models.CreateUser(app.DB, username, password) + if err != nil { + data := struct { + PageData + Error string + }{ + PageData: PageData{ + Title: "ThreadR - Sign Up", + Navbar: "signup", + LoggedIn: false, + ShowCookieBanner: true, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + Error: "Error creating user", + } + if err := app.Tmpl.ExecuteTemplate(w, "signup", data); err != nil { + log.Printf("Error executing template in SignupHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + return + } + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } + data := struct { + PageData + }{ + PageData: PageData{ + Title: "ThreadR - Sign Up", + Navbar: "signup", + LoggedIn: session.Values["user_id"] != nil, + ShowCookieBanner: true, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + } + if err := app.Tmpl.ExecuteTemplate(w, "signup", data); err != nil { + log.Printf("Error executing template in SignupHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/thread.go b/handlers/thread.go new file mode 100644 index 0000000..76bc122 --- /dev/null +++ b/handlers/thread.go @@ -0,0 +1,140 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + "threadr/models" + "github.com/gorilla/sessions" +) + +func ThreadHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + loggedIn := session.Values["user_id"] != nil + userID, _ := session.Values["user_id"].(int) + + threadIDStr := r.URL.Query().Get("id") + threadID, err := strconv.Atoi(threadIDStr) + if err != nil { + http.Error(w, "Invalid thread ID", http.StatusBadRequest) + return + } + + thread, err := models.GetThreadByID(app.DB, threadID) + if err != nil { + log.Printf("Error fetching thread: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if thread == nil { + http.Error(w, "Thread not found", http.StatusNotFound) + return + } + + board, err := models.GetBoardByID(app.DB, thread.BoardID) + if err != nil { + log.Printf("Error fetching board: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if board.Private { + if !loggedIn { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } + hasPerm, err := models.HasBoardPermission(app.DB, userID, board.ID, models.PermViewBoard) + if err != nil { + log.Printf("Error checking permission: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if !hasPerm { + http.Error(w, "You do not have permission to view this board", http.StatusForbidden) + return + } + } + + if r.Method == http.MethodPost && loggedIn { + action := r.URL.Query().Get("action") + if action == "submit" { + content := r.FormValue("content") + replyToStr := r.URL.Query().Get("to") + replyTo := 0 + if replyToStr != "" { + replyTo, err = strconv.Atoi(replyToStr) + if err != nil { + http.Error(w, "Invalid reply_to ID", http.StatusBadRequest) + return + } + } + if content == "" { + http.Error(w, "Content cannot be empty", http.StatusBadRequest) + return + } + if board.Private { + hasPerm, err := models.HasBoardPermission(app.DB, userID, board.ID, models.PermPostInBoard) + if err != nil { + log.Printf("Error checking permission: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if !hasPerm { + http.Error(w, "You do not have permission to post in this board", http.StatusForbidden) + return + } + } + post := models.Post{ + ThreadID: threadID, + UserID: userID, + Content: content, + ReplyTo: replyTo, + } + err = models.CreatePost(app.DB, post) + if err != nil { + log.Printf("Error creating post: %v", err) + http.Error(w, "Failed to create post", http.StatusInternalServerError) + return + } + http.Redirect(w, r, app.Config.ThreadrDir+"/thread/?id="+threadIDStr, http.StatusFound) + return + } + } + + posts, err := models.GetPostsByThreadID(app.DB, threadID) + if err != nil { + log.Printf("Error fetching posts: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + if thread.Type == "chat" { + for i, j := 0, len(posts)-1; i < j; i, j = i+1, j-1 { + posts[i], posts[j] = posts[j], posts[i] + } + } + + data := struct { + PageData + Thread models.Thread + Posts []models.Post + }{ + PageData: PageData{ + Title: "ThreadR - " + thread.Title, + Navbar: "boards", + LoggedIn: loggedIn, + ShowCookieBanner: true, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + Thread: *thread, + Posts: posts, + } + if err := app.Tmpl.ExecuteTemplate(w, "thread", data); err != nil { + log.Printf("Error executing template in ThreadHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/handlers/userhome.go b/handlers/userhome.go new file mode 100644 index 0000000..679aec8 --- /dev/null +++ b/handlers/userhome.go @@ -0,0 +1,49 @@ +package handlers + +import ( + "log" + "net/http" + "threadr/models" + "github.com/gorilla/sessions" +) + +func UserHomeHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + userID, ok := session.Values["user_id"].(int) + if !ok { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } + user, err := models.GetUserByID(app.DB, userID) + if err != nil { + log.Printf("Error fetching user in UserHomeHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if user == nil { + http.Error(w, "User not found", http.StatusNotFound) + return + } + data := struct { + PageData + Username string + }{ + PageData: PageData{ + Title: "ThreadR - User Home", + Navbar: "userhome", + LoggedIn: true, + ShowCookieBanner: false, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + Username: user.Username, + } + if err := app.Tmpl.ExecuteTemplate(w, "userhome", data); err != nil { + log.Printf("Error executing template in UserHomeHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..2bc8f85 --- /dev/null +++ b/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "os" + "path/filepath" + "threadr/handlers" + + "github.com/gorilla/sessions" + _ "github.com/go-sql-driver/mysql" +) + +func loadConfig(filename string) (*handlers.Config, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + var config handlers.Config + err = json.NewDecoder(file).Decode(&config) + return &config, err +} + +func main() { + config, err := loadConfig("config/config.json") + if err != nil { + log.Fatal("Error loading config:", err) + } + + dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s", config.DBUsername, config.DBPassword, config.DBServerHost, config.DBDatabase) + db, err := sql.Open("mysql", dsn) + if err != nil { + log.Fatal("Error connecting to database:", err) + } + defer db.Close() + + dir, err := os.Getwd() + if err != nil { + log.Fatal("Error getting working directory:", err) + } + + // Parse partial templates + tmpl := template.Must(template.ParseFiles( + filepath.Join(dir, "templates/partials/navbar.html"), + filepath.Join(dir, "templates/partials/cookie_banner.html"), + )) + + // Parse page-specific templates with unique names + tmpl, err = tmpl.ParseFiles( + filepath.Join(dir, "templates/pages/about.html"), + filepath.Join(dir, "templates/pages/board.html"), + filepath.Join(dir, "templates/pages/boards.html"), + filepath.Join(dir, "templates/pages/home.html"), + filepath.Join(dir, "templates/pages/login.html"), + filepath.Join(dir, "templates/pages/news.html"), + filepath.Join(dir, "templates/pages/profile.html"), + filepath.Join(dir, "templates/pages/profile_edit.html"), + filepath.Join(dir, "templates/pages/signup.html"), + filepath.Join(dir, "templates/pages/thread.html"), + filepath.Join(dir, "templates/pages/userhome.html"), + ) + if err != nil { + log.Fatal("Error parsing page templates:", err) + } + + store := sessions.NewCookieStore([]byte("secret-key")) // Replace with secure key in production + + app := &handlers.App{ + DB: db, + Store: store, + Config: config, + Tmpl: tmpl, + } + + fs := http.FileServer(http.Dir("static")) + http.Handle(config.ThreadrDir+"/static/", http.StripPrefix(config.ThreadrDir+"/static/", fs)) + + http.HandleFunc(config.ThreadrDir+"/", app.SessionMW(handlers.HomeHandler(app))) + http.HandleFunc(config.ThreadrDir+"/login/", app.SessionMW(handlers.LoginHandler(app))) + http.HandleFunc(config.ThreadrDir+"/logout/", app.SessionMW(handlers.LogoutHandler(app))) + http.HandleFunc(config.ThreadrDir+"/userhome/", app.SessionMW(app.RequireLoginMW(handlers.UserHomeHandler(app)))) + http.HandleFunc(config.ThreadrDir+"/boards/", app.SessionMW(handlers.BoardsHandler(app))) + http.HandleFunc(config.ThreadrDir+"/board/", app.SessionMW(handlers.BoardHandler(app))) + http.HandleFunc(config.ThreadrDir+"/thread/", app.SessionMW(handlers.ThreadHandler(app))) + http.HandleFunc(config.ThreadrDir+"/about/", app.SessionMW(handlers.AboutHandler(app))) + http.HandleFunc(config.ThreadrDir+"/profile/", app.SessionMW(app.RequireLoginMW(handlers.ProfileHandler(app)))) + http.HandleFunc(config.ThreadrDir+"/profile/edit/", app.SessionMW(app.RequireLoginMW(handlers.ProfileEditHandler(app)))) + http.HandleFunc(config.ThreadrDir+"/like/", app.SessionMW(app.RequireLoginMW(handlers.LikeHandler(app)))) + http.HandleFunc(config.ThreadrDir+"/news/", app.SessionMW(handlers.NewsHandler(app))) + http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app))) + http.HandleFunc(config.ThreadrDir+"/accept_cookie/", app.SessionMW(handlers.AcceptCookieHandler(app))) + + log.Println("Server starting on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} \ No newline at end of file diff --git a/models/board.go b/models/board.go new file mode 100644 index 0000000..ea2892b --- /dev/null +++ b/models/board.go @@ -0,0 +1,65 @@ +package models + +import ( + "database/sql" + "encoding/json" +) + +type Board struct { + ID int + Name string + Description string + Private bool + PublicVisible bool + PinnedThreads []int // Stored as JSON + CustomLandingPage string + ColorScheme string +} + +func GetBoardByID(db *sql.DB, id int) (*Board, error) { + query := "SELECT id, name, description, private, public_visible, pinned_threads, custom_landing_page, color_scheme FROM boards WHERE id = ?" + row := db.QueryRow(query, id) + board := &Board{} + var pinnedThreadsJSON string + err := row.Scan(&board.ID, &board.Name, &board.Description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &board.CustomLandingPage, &board.ColorScheme) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + if pinnedThreadsJSON != "" { + err = json.Unmarshal([]byte(pinnedThreadsJSON), &board.PinnedThreads) + if err != nil { + return nil, err + } + } + return board, nil +} + +func GetAllBoards(db *sql.DB, private bool) ([]Board, error) { + query := "SELECT id, name, description, private, public_visible, pinned_threads, custom_landing_page, color_scheme FROM boards WHERE private = ? ORDER BY id ASC" + rows, err := db.Query(query, private) + if err != nil { + return nil, err + } + defer rows.Close() + + var boards []Board + for rows.Next() { + board := Board{} + var pinnedThreadsJSON string + err := rows.Scan(&board.ID, &board.Name, &board.Description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &board.CustomLandingPage, &board.ColorScheme) + if err != nil { + return nil, err + } + if pinnedThreadsJSON != "" { + err = json.Unmarshal([]byte(pinnedThreadsJSON), &board.PinnedThreads) + if err != nil { + return nil, err + } + } + boards = append(boards, board) + } + return boards, nil +} \ No newline at end of file diff --git a/models/board_permission.go b/models/board_permission.go new file mode 100644 index 0000000..cc20560 --- /dev/null +++ b/models/board_permission.go @@ -0,0 +1,46 @@ +package models + +import "database/sql" + +type BoardPermission struct { + UserID int + BoardID int + Permissions int64 +} + +func GetBoardPermission(db *sql.DB, userID, boardID int) (*BoardPermission, error) { + query := "SELECT user_id, board_id, permissions FROM board_permissions WHERE user_id = ? AND board_id = ?" + row := db.QueryRow(query, userID, boardID) + bp := &BoardPermission{} + err := row.Scan(&bp.UserID, &bp.BoardID, &bp.Permissions) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return bp, nil +} + +func SetBoardPermission(db *sql.DB, bp BoardPermission) error { + query := "INSERT INTO board_permissions (user_id, board_id, permissions) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE permissions = ?" + _, err := db.Exec(query, bp.UserID, bp.BoardID, bp.Permissions, bp.Permissions) + return err +} + +const ( + PermPostInBoard int64 = 1 << 0 + PermModerateBoard int64 = 1 << 1 + PermViewBoard int64 = 1 << 2 +) + +func HasBoardPermission(db *sql.DB, userID, boardID int, perm int64) (bool, error) { + bp, err := GetBoardPermission(db, userID, boardID) + if err != nil { + return false, err + } + if bp == nil { + return false, nil + } + return bp.Permissions&perm != 0, nil +} \ No newline at end of file diff --git a/models/like.go b/models/like.go new file mode 100644 index 0000000..cd866a2 --- /dev/null +++ b/models/like.go @@ -0,0 +1,62 @@ +package models + +import "database/sql" + +type Like struct { + ID int + PostID int + UserID int + Type string // "like" or "dislike" +} + +func GetLikesByPostID(db *sql.DB, postID int) ([]Like, error) { + query := "SELECT id, post_id, user_id, type FROM likes WHERE post_id = ?" + rows, err := db.Query(query, postID) + if err != nil { + return nil, err + } + defer rows.Close() + + var likes []Like + for rows.Next() { + like := Like{} + err := rows.Scan(&like.ID, &like.PostID, &like.UserID, &like.Type) + if err != nil { + return nil, err + } + likes = append(likes, like) + } + return likes, nil +} + +func GetLikeByPostAndUser(db *sql.DB, postID, userID int) (*Like, error) { + query := "SELECT id, post_id, user_id, type FROM likes WHERE post_id = ? AND user_id = ?" + row := db.QueryRow(query, postID, userID) + like := &Like{} + err := row.Scan(&like.ID, &like.PostID, &like.UserID, &like.Type) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return like, nil +} + +func CreateLike(db *sql.DB, like Like) error { + query := "INSERT INTO likes (post_id, user_id, type) VALUES (?, ?, ?)" + _, err := db.Exec(query, like.PostID, like.UserID, like.Type) + return err +} + +func UpdateLikeType(db *sql.DB, postID, userID int, likeType string) error { + query := "UPDATE likes SET type = ? WHERE post_id = ? AND user_id = ?" + _, err := db.Exec(query, likeType, postID, userID) + return err +} + +func DeleteLike(db *sql.DB, postID, userID int) error { + query := "DELETE FROM likes WHERE post_id = ? AND user_id = ?" + _, err := db.Exec(query, postID, userID) + return err +} \ No newline at end of file diff --git a/models/notification.go b/models/notification.go new file mode 100644 index 0000000..105d21b --- /dev/null +++ b/models/notification.go @@ -0,0 +1,49 @@ +package models + +import ( + "database/sql" + "time" +) + +type Notification struct { + ID int + UserID int + Type string + RelatedID int + CreatedAt time.Time + Read bool +} + +func GetNotificationsByUserID(db *sql.DB, userID int) ([]Notification, error) { + query := "SELECT id, user_id, type, related_id, read, created_at FROM notifications WHERE user_id = ? ORDER BY created_at DESC" + rows, err := db.Query(query, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var notifications []Notification + for rows.Next() { + notification := Notification{} + err := rows.Scan(¬ification.ID, ¬ification.UserID, ¬ification.Type, ¬ification.RelatedID, ¬ification.Read, ¬ification.CreatedAt) + if err != nil { + return nil, err + } + notifications = append(notifications, notification) + } + return notifications, nil +} + +// Stubbed for future implementation +func CreateNotification(db *sql.DB, notification Notification) error { + query := "INSERT INTO notifications (user_id, type, related_id, read, created_at) VALUES (?, ?, ?, ?, NOW())" + _, err := db.Exec(query, notification.UserID, notification.Type, notification.RelatedID, notification.Read) + return err +} + +// Stubbed for future implementation +func MarkNotificationAsRead(db *sql.DB, id int) error { + query := "UPDATE notifications SET read = true WHERE id = ?" + _, err := db.Exec(query, id) + return err +} \ No newline at end of file diff --git a/models/post.go b/models/post.go new file mode 100644 index 0000000..fea607f --- /dev/null +++ b/models/post.go @@ -0,0 +1,45 @@ +package models + +import ( + "database/sql" + "time" +) + +type Post struct { + ID int + ThreadID int + UserID int + PostTime time.Time + EditTime *time.Time + Content string + AttachmentHash *int64 + AttachmentName *string + Title string + ReplyTo int +} + +func GetPostsByThreadID(db *sql.DB, threadID int) ([]Post, error) { + query := "SELECT id, thread_id, user_id, post_time, edit_time, content, attachment_hash, attachment_name, title, reply_to FROM posts WHERE thread_id = ? ORDER BY post_time ASC" + rows, err := db.Query(query, threadID) + if err != nil { + return nil, err + } + defer rows.Close() + + var posts []Post + for rows.Next() { + post := Post{} + err := rows.Scan(&post.ID, &post.ThreadID, &post.UserID, &post.PostTime, &post.EditTime, &post.Content, &post.AttachmentHash, &post.AttachmentName, &post.Title, &post.ReplyTo) + if err != nil { + return nil, err + } + posts = append(posts, post) + } + return posts, nil +} + +func CreatePost(db *sql.DB, post Post) error { + query := "INSERT INTO posts (thread_id, user_id, content, title, reply_to, post_time) VALUES (?, ?, ?, ?, ?, NOW())" + _, err := db.Exec(query, post.ThreadID, post.UserID, post.Content, post.Title, post.ReplyTo) + return err +} \ No newline at end of file diff --git a/models/reaction.go b/models/reaction.go new file mode 100644 index 0000000..2439950 --- /dev/null +++ b/models/reaction.go @@ -0,0 +1,44 @@ +package models + +import "database/sql" + +type Reaction struct { + ID int + PostID int + UserID int + Emoji string +} + +func GetReactionsByPostID(db *sql.DB, postID int) ([]Reaction, error) { + query := "SELECT id, post_id, user_id, emoji FROM reactions WHERE post_id = ?" + rows, err := db.Query(query, postID) + if err != nil { + return nil, err + } + defer rows.Close() + + var reactions []Reaction + for rows.Next() { + reaction := Reaction{} + err := rows.Scan(&reaction.ID, &reaction.PostID, &reaction.UserID, &reaction.Emoji) + if err != nil { + return nil, err + } + reactions = append(reactions, reaction) + } + return reactions, nil +} + +// Stubbed for future implementation +func CreateReaction(db *sql.DB, reaction Reaction) error { + query := "INSERT INTO reactions (post_id, user_id, emoji) VALUES (?, ?, ?)" + _, err := db.Exec(query, reaction.PostID, reaction.UserID, reaction.Emoji) + return err +} + +// Stubbed for future implementation +func DeleteReaction(db *sql.DB, postID, userID int, emoji string) error { + query := "DELETE FROM reactions WHERE post_id = ? AND user_id = ? AND emoji = ?" + _, err := db.Exec(query, postID, userID, emoji) + return err +} \ No newline at end of file diff --git a/models/repost.go b/models/repost.go new file mode 100644 index 0000000..bf9a2f1 --- /dev/null +++ b/models/repost.go @@ -0,0 +1,41 @@ +package models + +import ( + "database/sql" + "time" +) + +type Repost struct { + ID int + ThreadID int + BoardID int + UserID int + CreatedAt time.Time +} + +func GetRepostsByThreadID(db *sql.DB, threadID int) ([]Repost, error) { + query := "SELECT id, thread_id, board_id, user_id, created_at FROM reposts WHERE thread_id = ?" + rows, err := db.Query(query, threadID) + if err != nil { + return nil, err + } + defer rows.Close() + + var reposts []Repost + for rows.Next() { + repost := Repost{} + err := rows.Scan(&repost.ID, &repost.ThreadID, &repost.BoardID, &repost.UserID, &repost.CreatedAt) + if err != nil { + return nil, err + } + reposts = append(reposts, repost) + } + return reposts, nil +} + +// Stubbed for future implementation +func CreateRepost(db *sql.DB, repost Repost) error { + query := "INSERT INTO reposts (thread_id, board_id, user_id, created_at) VALUES (?, ?, ?, NOW())" + _, err := db.Exec(query, repost.ThreadID, repost.BoardID, repost.UserID) + return err +} \ No newline at end of file diff --git a/models/sla.html b/models/sla.html new file mode 100644 index 0000000..c227d0b --- /dev/null +++ b/models/sla.html @@ -0,0 +1,28 @@ + + + + + + Document + + +

Lista de frutas

+
+
+ + + \ No newline at end of file diff --git a/models/thread.go b/models/thread.go new file mode 100644 index 0000000..a357c41 --- /dev/null +++ b/models/thread.go @@ -0,0 +1,57 @@ +package models + +import ( + "database/sql" + "time" +) + +type Thread struct { + ID int + BoardID int + Title string + Type string // "classic", "chat", "question" + CreatedAt time.Time + UpdatedAt time.Time + CreatedByUserID int + AcceptedAnswerPostID *int +} + +func GetThreadByID(db *sql.DB, id int) (*Thread, error) { + query := "SELECT id, board_id, title, type, created_at, updated_at, created_by_user_id, accepted_answer_post_id FROM threads WHERE id = ?" + row := db.QueryRow(query, id) + thread := &Thread{} + err := row.Scan(&thread.ID, &thread.BoardID, &thread.Title, &thread.Type, &thread.CreatedAt, &thread.UpdatedAt, &thread.CreatedByUserID, &thread.AcceptedAnswerPostID) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return thread, nil +} + +func GetThreadsByBoardID(db *sql.DB, boardID int) ([]Thread, error) { + query := "SELECT id, board_id, title, type, created_at, updated_at, created_by_user_id, accepted_answer_post_id FROM threads WHERE board_id = ? ORDER BY updated_at DESC" + rows, err := db.Query(query, boardID) + if err != nil { + return nil, err + } + defer rows.Close() + + var threads []Thread + for rows.Next() { + thread := Thread{} + err := rows.Scan(&thread.ID, &thread.BoardID, &thread.Title, &thread.Type, &thread.CreatedAt, &thread.UpdatedAt, &thread.CreatedByUserID, &thread.AcceptedAnswerPostID) + if err != nil { + return nil, err + } + threads = append(threads, thread) + } + return threads, nil +} + +func CreateThread(db *sql.DB, thread Thread) error { + query := "INSERT INTO threads (board_id, title, type, created_by_user_id, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())" + _, err := db.Exec(query, thread.BoardID, thread.Title, thread.Type, thread.CreatedByUserID) + return err +} \ No newline at end of file diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..4ec582e --- /dev/null +++ b/models/user.go @@ -0,0 +1,89 @@ +package models + +import ( + "crypto/sha256" + "database/sql" + "fmt" + "time" +) + +type User struct { + ID int + Username string + DisplayName string + PfpURL string + Bio string + AuthenticationString string + AuthenticationSalt string + AuthenticationAlgorithm string + CreatedAt time.Time + UpdatedAt time.Time + Verified bool + Permissions int64 +} + +func GetUserByID(db *sql.DB, id int) (*User, error) { + query := "SELECT id, username, display_name, pfp_url, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE id = ?" + row := db.QueryRow(query, id) + user := &User{} + err := row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.PfpURL, &user.Bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &user.CreatedAt, &user.UpdatedAt, &user.Verified, &user.Permissions) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return user, nil +} + +func GetUserByUsername(db *sql.DB, username string) (*User, error) { + query := "SELECT id, username, display_name, pfp_url, bio, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions FROM users WHERE username = ?" + row := db.QueryRow(query, username) + user := &User{} + err := row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.PfpURL, &user.Bio, &user.AuthenticationString, &user.AuthenticationSalt, &user.AuthenticationAlgorithm, &user.CreatedAt, &user.UpdatedAt, &user.Verified, &user.Permissions) + if err != nil { + return nil, err + } + return user, nil +} + +func CheckPassword(password, salt, algorithm, hash string) bool { + if algorithm != "sha256" { + return false + } + computedHash := HashPassword(password, salt, algorithm) + return computedHash == hash +} + +func HashPassword(password, salt, algorithm string) string { + if algorithm != "sha256" { + return "" + } + data := password + salt + hash := sha256.Sum256([]byte(data)) + return fmt.Sprintf("%x", hash) +} + +func CreateUser(db *sql.DB, username, password string) error { + salt := "random-salt" // Replace with secure random generation + algorithm := "sha256" + hash := HashPassword(password, salt, algorithm) + query := "INSERT INTO users (username, authentication_string, authentication_salt, authentication_algorithm, created_at, updated_at, verified, permissions) VALUES (?, ?, ?, ?, NOW(), NOW(), ?, 0)" + _, err := db.Exec(query, username, hash, salt, algorithm, false) + return err +} + +func UpdateUserProfile(db *sql.DB, userID int, displayName, pfpURL, bio string) error { + query := "UPDATE users SET display_name = ?, pfp_url = ?, bio = ?, updated_at = NOW() WHERE id = ?" + _, err := db.Exec(query, displayName, pfpURL, bio, userID) + return err +} + +const ( + PermCreateBoard int64 = 1 << 0 + PermManageUsers int64 = 1 << 1 +) + +func HasGlobalPermission(user *User, perm int64) bool { + return user.Permissions&perm != 0 +} \ No newline at end of file diff --git a/static/img/ThreadR.png b/static/img/ThreadR.png new file mode 100644 index 0000000000000000000000000000000000000000..ce49d63911f3a06316d61e1057c9112ad103d10f GIT binary patch literal 95410 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%0Dgo9yEbz4e~mo&^Q^fNp+=bYY* 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%0A?!MXa!?a;$ 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

Fb63?U?QtKM;;7 zHt;E#}XGs7lUOaJoh-O>x$+{2x*Ho3J<@Xe_gyUKXXs;Gs_ z8A6|UVXSzwkSMWrO^?eit@SfsSS=jW3bFKS#B%#4m2r(7YnClCPU23}pE zv~mpoMGd`2qihk&`5?`wfD-NXJCAH>X!_4q%2&*(snz32$uUgFe!X~@bJM4HXuR33 zIzK%0g=h!;nWp=#{nzW$Yy10;$-V7Z87A5gwsrsjxh#3mCIjHr0O+g&fV{pb$EEb;@S4q5By~#Fn629Ft(?h5PT(N!eFg z*Jop@Mi(s6UC+}B#@DTP`2*&J*@5cLNPCUwD%dasl7D4|}SJvujy%ZCfQtD{0@ul|w+v=L3ac2c{R2 zTi{&#<~9~;H2n92In4AihOEZR7a(M=NGdRJh|5yVYQNGF!U|~{(dtjw_KtRYs8s6l z+7=?3%xw-5azN++@m-K#f*dMy9aqaQ5TC=GSUAaokI6Ewr+LH|Y+;{)D zUw=Mww|do34B)E7{e!iM>BD+^mdEV3$f1PrP`4jihXM^LKl>A8iSBE2J*mP{!Dao9 z^~W}JWjf{;-6=);;)!Wdg?!A@_u?D2{)5I^JviJT;mB?#^ea@pdEj-!&AGMKG2*2i zql6DpAnXtBi`galIA~t5+ZHCBwzc;BoN)^Ne-?n8vcYMLOS8v+3AqM$fS*@4mz2%P z4CKj6Bx3lM1(mJ!n`UZd_@_DAaBmq2{Iq(k+ge|~&ljc4UK371w(p6mZ&_!Gkoy%Zw<4`__A@7*GkNqN=uS}EyB?ti3chWv zC{#{iG`YjJse2Uv!G=n%CzJG3!H`GupZnnOz_}I>P<#D0zstW(6uH-bniIFxO?@6j zA?Fha;LhCJ+3X5yY$k7VZ$sOh*zAu${hJfYKp>x1Eb0_~Tj0`)ZOR)IlC{6xug4wp zOk2AS2fq!Be38*Kw6da7uL^mJ<*`@oGUJ+Kj_VyNU3vL3l_}OWzI2OF^GGWK>PggY zvArr&@;J8!`%wt3USk^&$mznV0D`HePQ_kqgS+!_XHxg$;@h(;|9y}ftbEHwv;AF> zp3UN)(2fIh%IK|gH~w-PVeJm=a688amF8E_gq3PCB1*mbag)1pjh0h_icLOZH3jO7 z)eOHk$a?zn(819idwgm?3G-dsSCpM@rTZR8_Z2f-2ZP89hivQ#dS2%XFR~t0!DKzD z%9vjSYv%LL|85QAebyB!8eqy{-qN&~-BhTUg4QTvO-#)pw!a~Ow$;a!%ODcys&+U)L z)nq9wiWb+L_Sde%0t97sPgnV}jF&(~s`g#>QH0-KwOAkJgit_gwY%S;j-CMnt=wnXj$2{RpKy)PRUQ3j``q%h74i(l)VWAiJl7D#jx+XuC?wfMHy8FJT;nC8{zqp6R3b~Wb z<0Yu8#0G#*9(k-tiZvtNa^CU(TjOl*X;PK zMM57cZ;HZ4^X3&LVdK6&G`4(d?(i?_y09Obb;7sIT|*;_kGFJYpuqy;)@Zyyi<{Jt zFD8V)%A5nfSHsW$J~at=~<%Pn7+O;u*)VI=|ndGaJotaxL8s=1;=n_8$Eg zsO32``Bwx=BaNl|&W6g)=t1P}bd9`o9!nh_qV3Pc7j4s_(XW|~1nBHL=oErCBo_)V zPL~k>HOS@qFg{B1IGY+%|H&ogsjj7Op;7!5_lGpOZ2{}$hsWwZ;nDdq|ixEF)cjrjApYnNRm0=!u zZv~Vn+VSE|89(^KWnl-JI%)BX5L__Dfg{Ps0)?S$*XD#&0CAKx?kMMh!RJ7*FG2=C(qC?=(f%%!5L(Nwo0ZQdu zI)w`8P+2)e<%cmpM3Nl4TEyz=OS}=BXUNOD#i55lF(RfvCqQ`*fg@9!Tki%Z56a(R zntDwQ-M_EAx>nQC9O2_$_>U9+PJ3LY)|+TA6BY}8Y2c+=$E8|T(s~N$f|GR^rgc!s z(d5SqoGy%xn9?O$x}(PO;W_xRbOG&gSuL$_YPT_By9T;kBmJXFrs)j=hw92UN&P?gDYe<8f0%?M2KBWdG1KsY>hmfQAb9l&ujU%tbN0V}h96|$ zkS@B1-b2qTtVoDVn)c@P+Q-Z=REgjAe#-hZ9>cBftXRAXKQ7NJEU{*B{L}R>3PK-{ zuUITbvnx2dHA%4f^wh2E{&l&@`~ZE_acTLrur%MAC}HB!CF(Ys)%%T6uN1Onn~Sx6 zlPTHSqh7CXo}UW%bVi#=gmFz?lFxdKdoSDy|F(J|wpF6L(>z(FG^VP(UizG;d0bS^ zWZvs{l0WCO(*ugQ$*jwUXINsi3nlNb+HPMJCY0X79XEa@-PL+c0Ayi#-UuCP&i<^c zBcqMCsL{&L=;=}Or{i#Spnn!0h(=1el6*)m%8t?P7fJHM^*9ilnIMRVpz`=Bip4B> zr^C&K^Fxj+OZlP)!*So<)OmukZIr?9L{Z%qHUqCd(Jy)Y>FsXIaP-Kfaw8-PiPd72 zsq}`c$I=%EnDaKvC}6W08fgPh^dcL;xaBUD;qg|b>-fMyg#N*-kquX_afIbvR8O{H zNAK{z<$vqLOVNrQZV-F($ZtlRMi#zs*nZmPTQf}(8bNQF zcb{ml=Ub>_`PGg9sk;@?Y*0GL6Deb9nWTKUdI7RX(D1GFyVveeaLCI0OHA*1n@bhP zbJw~FV4DxK%yLza@!Fr6(y<@kUjPau_7mBF+@Hvv+UBhq$V7|oMPN{o@MK{+IJSme zUvAEA-$wYyKK2wC3zVhP{m%^!)=Ck7=3q77o1gE*Hs?7a_P!EyOkjv+3g3Fy?BPx0 zu(HHB&+(q%t0f-2vXmj{r9Z~)NW*m-(^U~iQTE+L{!KHR3$vxEgMaIzmM+c@U0>)n zzFszCVT~XE%3iE)5t$ZTrx(aO>ci%+j#$20galtTXy#3ZE1tT<-OC$EilW^~iY zkpFhv@f5H2vgF~1&A!(7x9Ip3L@m>&uyDW6bXrQCywpiZf7)V<82-g8S}6(Ay6fWl zCI@#{N|Ev4xx)VQ{`2k4c5dGFwfrOwc{pvij9xo`9Lzz zZO^7aMAEAxSv42`p-S~9Pab2tk6u_*@Ilg?rPR~68RN{;GjnNA_>BvB+qRd9eFe)+ zF);G&Hk9-B3>BN&L$O-0LwA*gtYpJT2VSjBs@)Sauz7m=u_w zFNBNI_AzaP^(RpDWKF^pq&q&#cw{B-A}a6>gFn~QTk@D>M%MaKy=~t|ldpRmA~CWIE9CWxvriYZWuid8+JTm0f^TcoU}$W(DTZ3>d0YYwuo|sW%(y8eLPG$Z1H%gwjXgGV&{CJ^eWbAuBD(M*|t?}u@bFB*hfydT>^Jq@(+ zj$XHa=Ov)m{FFiZwYg{?T+c|rzHUt)z85gSpXN?UWBJ03NN=+SmT7IGfe5}}JU9=^ z%4JwH;juV(!(*8f7##PxUd!~ikMjj6*jvC>W0m$xWCgU7zx>8%-t}#w9NO=b9*+Z=^#6~~Jz&H8_QwzciI2hEvzc20BIpi8m)=< zhY-40IAd#jj_o80Igj}-5+hnl1z59rNJd;Qx-<>=k;QH%GSdEQLk-D|f)98C4%{|74-nVG# z`KzOB5t*Qt$CT1H)%Ot>N0khopphoiE1Tp>zKY^dr~i-ujar3)@H1=2)zjhx1#hM} zbN#KkcCjh8CtM-xF`lv8(&-Hag^JuN`EN*gPD&=X3nsKOs14;8;XK&r*InyuV!YK_ zI&FIz!{&EN?Pxyv4ngg?Ki3K`DXXBelU^{8K4s?bjtzSu`2BaRw6;e~eq*W#iz@YLAVvX`{K^*&s{obzYEgC<4tmDnc`AryJZt6bVUePr8TFA z7QNtQMSkY|jCNjYvS#%3SD{oNZ{F8>#)-sCB}%f!OJ4gYg#QQ9<-_bssyLpadW-Ca z8@z05@QMz8Le3J|^f}|gD2L`zR=cURH#SP1?u0Lzs3Hhv#=7u2keq+jVqZ(u#0-T= z*FbKx27?*=_v3$_#!qf1)vAjnIWISXK*Pfvx_ zeCl5Pru~=n1@qF0xc;?%s%?hz14A%qd{rLG(3D?SUO$HV)wTBW3lg5$CD&6(`k|!N z)<41Z{kRtiYIRGu*eI7<>e@J=cJ+0r4mo!oj=7}So(F>$!bWNPuRBos5W0#F@*J$| z*60ooS;edtOVBEv&vZ1xij z?7T{xh(XW@VmfrhL*cJHf$V&`bPJsD{ zGu;uiqnbpkuKc&W?y*$|wPNV(0)g&b0<%bq@<&_p1Z%wrNwY|td_cp882AE!raVnNV zL($*|27+@f5Cz{447BVv4aS&7(=o-T6rhfVIe6~PF&>G=eh_z8fd;mIh7Qu zj4>IQG>?AdHaZrW-BpNSY@V`R6?Ga$kdbaQb(GaA9w+{rvdzl*kU!(K<6^zSNzF$d znyh_1j5yw!^EIe7ur$8vzq`1XAf;`>pq~{K^Y_{Po3_%QPikERG7Fq>dEY!Jc-Fj2 z&O(FKED|M8PSRdq;X_9?Y+v7hvNrrWS-j7iM7W>&?Su#An)9KY839#d8Njrk3_L^7*~xM|FRKLa9WE$!3kqcIC8d9;T~J@$ItU zpQjpTUgxe2rV7goq6CQ5W5+V>4G>nKZyL&%iKVxi;t7?Q@y6g6&F(=?iRLl84>og~L&e}k9U7oi zn|GGN{*tZwU}}zg%@Ji7MAcMfhldkQCtD7b>ptPkSI5USo3c$AtxS{Ebcm_@^cyM0 zuqa>I!}iiY4D}N;Ez%lBQ)T)}Bv}luK3myoS!t3zAE!-4ro?49)J|ZvjhdE%(~Q4+ zGTC>8UBkdimR+-)712gtb86JWs3t9Py3q47k;Gmhu$b`M##SUCLbZ`B~bV75$E;M-XaB!y&obT;js! zaOlvg7i5K<&I4gCcx^an$C5#iecuv%SQ=7DJet1o3wz2q`87-Qekd|yB=2pWRvbbj zxQS`q0lqXo8GeVwjk~i0{tA(F47&QOv-!^w%WB#A3h1olv<^uD3uxMC*|OoXk6^Rn#^3Cu=%J4Gs8NmH>*E@NberA-~zE(sg zM9z4>^q{GCsvn&C7>g%8fl5QqbKQw)=E0bd}-5hPs5C^aGd&N(C zUi!cMC2I1LqPcl-Z$TBV-p^h*N+N81nq81GdTviRJR& z<1t(k5=l{jaDr+~Z72WcH3io5Abn@&+Q6gl3hkcvfIg*1dt%p1$|R6B|BRub@-HF9 z-&t0A_(ZOC;^|H8Y4xO%kMhcee8qa&F(t^qr~2JO6)c6F%bFUzrMMp(Q9gFUGus@#(h=6P-IheDY zLOCI4s6CvHL&5kKLq8YHM2%lNzb3ccFRIfT3YeoO^(3FpD``AC;3i~bZMjOr2l9_OLP|_Av2dQr_obw~~SyN2U7*4i#?8{u(|G`_?V{IKE zbZU=a!3mG_K`ep~@K-v7|CfSiudZw2+Zn;hkm%Am`8_6>YEO}|M6DZ3m$+=K@KD;m zTAJH&RQfaf0L5lQ83W9II!>NKYu4lxRmO*2$xb4)pyBfI?EPt2=V*9!|O|JrSa{Sh!KX)xi(XxAV3qTTi$!(ud7@@1r2OSEzb%zne&4zNx344#U}*gNE_bGnpIYpE%< z$D?J^x_XlVXwOjXV(=g$p(-_@RMaOE-Syqh28YW&<7Xy0y#tY#T3UDoLy{$oa;8#m z=V3kYQ)Bv4ikbxTIDcOt6W61`%>rzcOj^yoS*PkDTi25ilavVhIucM)3eS>%GDVNC z7|Au6Gm}CH?7AF3zE!4{tXxp|C7sIX^6+L|Q9&C=U#tuibb?#7B0Lwft>86}SRn?7 zznFP@1tJ9))rAFW=|qyW4}RV6=fs8@Whi?!oPrZVXcD7hqCPW(gzIl#7QOdaD9Dfi zIOGGh>`RpN5eQg6vOhoPI&I(tdv|ZAimKlGd{eJa0R`!bLv}^_C*nu09OYm{hVV@# z?Ud3lLlHfN<_hka^^IwGzadT)^x zWV92zmMw~cf}}Gb3snzi7w7uT3KhMdJ2|!8xK&~1!jdpVg{~m@POS<&Ka#agEd52$ z32e5xQ{xiRJ=Nog_5}j3Y8AFMr=crf=*d~cN(V&8jYAPDc%xaZIScx9eDz-51 zb1&&wB2lmL_(G5I`4ph$^h|MN5AviR*^{{|e8WgJd5WY7?px%tSWkpyPt zp)oRA_2J4MWsRkC zd84@L_l|*1bytsy{?Z|;*+>lCCtsu^`ryv=BlI<44XuQMM}$#89$b#!21V}iq+Zsg zp(}O%iB)67939T7dHq-C|7QX4c85_kH3>^S;F%QgciE@Bm4*=4l{?MGsRXG89Y1Fy zdnRW1@7@Wd6{HO^C08q9^Q)6_rCHgW%{;MLv7>jc^>{&Px+sQ%!o5}R^V|K4wi)?A znAzQHaSvsF$^K`T%e0h~%*X}_8pwO@hsh8^e9nOKl=Ve0xaQhsnqe^GhVYUyWS%=Latv@1jD5$ODWPs?`3d)Up5Dvk`u9?K|E(XUMc)ZMh8OnCfZex zNj2mqIUPkvn{FE-BfF5ZiQ4~MjC_9awGAJ=FWNjM;Vn(xH;fP6!X|wn=_hdpzDw% zxvpLG&={R9Ll{ZGoci>493Bpjy!Oo-6C;eRffIrcYUfYjEMc?AjiGOY<%K{l~#Sr1DRKiw9S3m zH0XJRXAdSw!G-H}=!&%F>VtqV6v?>JK8M%3s7rRthXi}|9HxV8|EZY1SYQI*Kcmz2 z-&UApK9@^NFhyE`!F8g2fOLLrB1_>~u4l)Doc9ITKF^uc6{7FItL2UjN}_z$dv+s2 z$#D>=SfL4%!K~8u0&1bs?3v zsNTlk@>DXD_sq{aC*E!CH-fQ=$SJ=VvuC5_Kv%aGG#0A^%?YA70i{Rhf;=_{4d)Z2 z5L5dN&fMDFQ_?&!o&`|Uw< z-Qsmg(()7(sE+!w?=06juX0i_;)GP2pAWSp6zE4W=_x4!rx?f(x>>hYw{ArCqUw>c zdghT*_oT!CYOk4Grwah}H~HzV>qd8=>d3C0*bqF;5Ef#fR3V}qUgIIU93pDNrT=ia zIzI0VTmVcp2JCj?G$>?=BgOwE@2E}{vwK!bz9NR0s6=0d0ISLU*_Phck7(&Azh1u* zk5HPz)*;9%_kTR2A@9AZJVR#H=Ri(|a!C??{r7)SaE`I#uFl@3&UG)dibB=GXTEq5 z^BFtCZNF`d%=50q`|%83dSAoz1IR#elbT#bVyw7LU42ULPXD4#ZxpO6qK#}bcMhBi zbo20YXAf*|jFyWc`YLWAPrANZBgVRQra=_j(Fp;#akG_Q_sm^;d;QD`Kg{iFHeh?1 z9;DuQ(S1xgtipxc@UDN(%AvGL#PCMqz^KP($bwxTkfGc=xcM| zj!YgmEIw+1Teoy9^i>mg534x7rZk7B2NYiwWIt)4wyzUmSv4$h__lvo1sbBm!$+E~ zJq=0Kgd~00m1f6Z#j+!5h5CMt7~_MdqUDLG$o11hLHgL-7idJaDUfW3zjb?Qx@@|D7gW~JEp z`9|UD_|M%ON%y0ZRK&~cE#Z9>5HR7;Eo=iH76Y;)MtVyoE2Z(6(47>Ih$ah@PvrCC zCk#&rXy$D>@9&CoJrwTWip{lMv6~l8+!&>2CB^00X=nw}ldhu?vMz3wnGT95FeDO8 z?ySc?b%XaUTzhI2P?Sa`pGaY4i(9R|>cRSSMJ~wMFnzQmi~{o&Fb0)5y>Vb+q5$6> z6ECExS-9V}u#`62RT8{Poa7Rz!pf`K>Z{$E3pggMnbGkSevlhB8T<%Kj4m(q8|3+# z(ec8tamTGFm9pm!QN@$n%)MdHEz4KEuic?xfF1Ij4GoMrv6n|ogwi;vwbYEU=M^3o z&V_yul`fxPK$*Jot^TVDtP?(7*Ka)x=q@;_wUlPQlKb{(ZG(xWo8Kj~? zx)7<*u3P7?A!-x~;j@B~Q5CWL6UgCy7)6_z8ubStCMSskN{5&(Rb79{@t*>F^CXB} z1jz(A@bvPg?xa-$geW)HH$LEpT3IGXB?b&~;MobsH4

>Mz3PE#7kLw<+cV3h}$eUkpRJ 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 literal 0 HcmV?d00001 diff --git a/static/img/ThreadR.svg b/static/img/ThreadR.svg new file mode 100644 index 0000000..73eb9be --- /dev/null +++ b/static/img/ThreadR.svg @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/img/ThreadR_Home.svg b/static/img/ThreadR_Home.svg new file mode 100644 index 0000000..87a8071 --- /dev/null +++ b/static/img/ThreadR_Home.svg @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/img/favicon-32x32.png b/static/img/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..45d52dffe9797be101b5327a72234c361ea86bd8 GIT binary patch literal 752 zcmVnd9*i22l-WSS2YJs8E3 zrG!)%ow{X^1RXl{7j*36rRdNhBCyz#WJL%SA{K_2tE*X{yShH^y6*FI2rSWXcLY7O zzuV03H{Z`|elx#$&^-9+R@lWR>}GfxkOlyV$qtN$!vyAic-&(Ie6z%&Q3OFi(28g! zk?cGh`m8K;hD=0RMk`4Of`CT&eU8yk2;a|13Mwkm88VXxD2l?O*Td`1hj?7yn3)(S z9Pn=#XE0mHFdC6F42ZIfD9LF40>mN_Y-I;2tg1-*Ijo`R-hit2L)BfpE7oVLkv6Yt7 zaOGM`;gG~C*kjE@*6aS+KivC{xgV31pEylUett^fNdYR)oI_DIe_aF?=IMR<1WB*s zSle|(NlqEC1=L`ySX`d&Q4V75@x(z>?X zb__5v(2r+y1Y2VC$D$=5!vUqU-0000?K$l!hqjcGjynSDpp-%x0|hoN0#S*V46`}IVl<2K$rz0f#s_1vWF}@$ zzW88#@Jcjp(Ouk(W0P@*F%ic7P75?3;Ora-f`CXOfPE{py9_BxSYWm+Ywq7 zYTiE$g<9bbz&m(+@!_`^=os`)PE$~7lu$+~{$Ae_NJtVq&%?>_;CUXr!@2p{!NWW_ zjfcx?eovaDNF-*&!&GX7>B(%jxY`1^{K*x;ru6xB=`2yY0n+U1!1CIq(i15=sI(Oa_%!iOA?kh$Uk0S;#Uxjn?b! zTVhn42IVy+$S%o(NbFmtt^4K<*bUZA)8(Whw=xF+K+)9RrO~z<_qGH>K|0R7cM6ou z_X{k}Eura3n?SF6{i(pzfzdxk0RYLy$8h$8mmw1QSKn@ZeH;9??3Gn)Kuk(>U@Ea~ zb~D92{aU5plV6%&M*Gc9fnHuy7PwY|V>w7^3W@r7zX!bYUA@5jnMHaWO40_dl~AY^ za60)NXBe-U$F_==tL*~4>`X~uo4EviJeL1l#oCI0IlKN+BOIK=O-nT;BTg3^Sb)sJ z3;@9N$c)be+1Ko7xzZ}oPgEBLz8lRd$;9%)GBmLop9gBW-iGyub~i05PJ^^OUEl!- zg7C{zyo1NxADRUE@#;dq<7!_vG+n=SV`+ZbO_PVy$gRi@jU8FTWuf<|P=kr5jPki^1(EC}%hOK@*r~ww|meAAKE70?+azo=(qV-xEoKB~E z>WtMO*_a&s0K9`o>(3nmJ@1+Q^S)mI51RY1vasqtn3zCrr71K3TCWM4HbX%M7!?!z z088^r80s7r2Jb#EAD;m_Zg#@8?bJn~Rv#_-p%a`fO*J*IrF?g z&n`6{$U)bx0q)=G5@i1G~-!2f);TS+{3aJgK;1K`OxyU@e4<5qliz81L^CP?Md-~g~r&3V)(`^|1%HV<0x={uhR04iQ7 z3wR9d_$j5$YV)EmqV}WjVJOmLta}`dKefQgyYS*$FYL>oGnZHHEmT4!AsI_v(e z(8gi|@~e&oR~8a^dBn5b64T>w>g|)kmqyixRR8;%Bz*$TU3fL%f$I~1p&-NScLV{H zzH|a_Ts#L^m~3AQ*^XntSZsi)IScn6S?eM(0sZ6YD6c6+Y--GbRAOSc|5mY1Sg|l; w1I^HgNr~RGr;P!>1OJ#dQ44LN7QteF0esZ7S2hc3YXATM07*qoM6N<$f=pV61poj5 literal 0 HcmV?d00001 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..d5d8e4d --- /dev/null +++ b/static/style.css @@ -0,0 +1,186 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #fef6e4; /* beige */ + color: #001858; /* blue */ +} + +main { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +main > header { + text-align: center; + margin-bottom: 1em; +} + +main > section { + margin: 1em; + padding: 1em; + border: 1px solid #001858; + border-radius: 5px; + background-color: #f3d2c1; /* orange */ + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); +} + +main > div > article { + border: 1px solid #001858; + padding: 1em; + margin-bottom: 1em; + background-color: #fef6e4; + border-radius: 5px; + box-shadow: inset 0px 8px 16px 0px rgba(0,0,0,0.2); +} + +article > header { + border-bottom: 1px solid #001858; + background-color: #001858; + color: #fef6e4; + padding: 0.5em; + margin: -1em -1em 1em -1em; +} + +ul.topnav { + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; + background-color: #001858; + position: fixed; + top: 0; + width: 100%; + box-shadow: 0 0.7em 1.2em 0 rgba(0,0,0,0.2); +} + +ul.topnav li { + float: left; +} + +ul.topnav li a { + display: block; + color: #fef6e4; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.topnav li a:hover { + background-color: #8bd3dd; /* cyan */ +} + +ul.topnav li a.active { + background-color: #f582ae; /* pink */ +} + +div.topnav { + height: 3em; +} + +div.banner { + position: fixed; + bottom: 0; + width: 100%; + background-color: #001858; + padding: 10px; + text-align: center; +} + +div.banner p { + color: #fef6e4; + margin: 0; +} + +div.banner a { + color: #8bd3dd; +} + +form { + display: flex; + flex-direction: column; +} + +input, textarea, select { + margin: 0.5em 0; + padding: 0.5em; + border: 1px solid #001858; + border-radius: 4px; + background-color: #fef6e4; + color: #001858; +} + +input[type="submit"] { + background-color: #001858; + color: #fef6e4; + cursor: pointer; +} + +input[type="submit"]:hover { + background-color: #8bd3dd; +} + +button { + margin: 0.5em 0; + padding: 0.5em; + border: none; + border-radius: 4px; + background-color: #001858; + color: #fef6e4; + cursor: pointer; +} + +button:hover { + background-color: #8bd3dd; +} + +img { + max-width: 100%; +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #333; + color: #fef6e4; + } + main > section { + background-color: #555; + border-color: #fef6e4; + } + main > div > article { + background-color: #444; + border-color: #fef6e4; + } + article > header { + background-color: #222; + border-color: #fef6e4; + } + input, textarea, select { + background-color: #444; + color: #fef6e4; + border-color: #fef6e4; + } + input[type="submit"], button { + background-color: #fef6e4; + color: #001858; + } + input[type="submit"]:hover, button:hover { + background-color: #8bd3dd; + } +} + +@media (max-width: 600px) { + ul.topnav li { + float: none; + width: 100%; + } + main { + padding: 10px; + } + main > section { + margin: 0.5em; + padding: 0.5em; + } +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..85b8843 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,16 @@ +{{define "base"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+ {{block "content" .}}{{end}} +
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/about.html b/templates/pages/about.html new file mode 100644 index 0000000..6802192 --- /dev/null +++ b/templates/pages/about.html @@ -0,0 +1,21 @@ +{{define "about"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

About ThreadR

+
+
+ {{.AboutContent}} +
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/board.html b/templates/pages/board.html new file mode 100644 index 0000000..3d25e5d --- /dev/null +++ b/templates/pages/board.html @@ -0,0 +1,42 @@ +{{define "board"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

{{.Board.Name}}

+

{{.Board.Description}}

+
+
+ +
+ {{if .LoggedIn}} +
+

Create New Thread

+
+ +
+ +
+ +
+
+ {{end}} +
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/boards.html b/templates/pages/boards.html new file mode 100644 index 0000000..9960c04 --- /dev/null +++ b/templates/pages/boards.html @@ -0,0 +1,36 @@ +{{define "boards"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

Boards

+
+
+

Public Boards

+
    + {{range .PublicBoards}} +
  • {{.Name}}
  • + {{end}} +
+
+ {{if .LoggedIn}} +
+

Private Boards

+
    + {{range .PrivateBoards}} +
  • {{.Name}}
  • + {{end}} +
+
+ {{end}} +
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/home.html b/templates/pages/home.html new file mode 100644 index 0000000..cba7c6d --- /dev/null +++ b/templates/pages/home.html @@ -0,0 +1,21 @@ +{{define "home"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

Welcome to ThreadR

+
+
+ ThreadR +
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/login.html b/templates/pages/login.html new file mode 100644 index 0000000..d27f2f2 --- /dev/null +++ b/templates/pages/login.html @@ -0,0 +1,30 @@ +{{define "login"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

Login

+
+
+ {{if .Error}} +

{{.Error}}

+ {{end}} +
+ +
+ +
+ +
+
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/news.html b/templates/pages/news.html new file mode 100644 index 0000000..74c3fc4 --- /dev/null +++ b/templates/pages/news.html @@ -0,0 +1,25 @@ +{{define "news"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

News

+
+
+
    + {{range .News}} +
  • {{.}}
  • + {{end}} +
+
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/profile.html b/templates/pages/profile.html new file mode 100644 index 0000000..67ca93a --- /dev/null +++ b/templates/pages/profile.html @@ -0,0 +1,30 @@ +{{define "profile"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

Profile

+
+
+

Username: {{.User.Username}}

+

Display Name: {{.DisplayName}}

+ {{if .User.PfpURL}} + Profile Picture + {{end}} +

Bio: {{.User.Bio}}

+

Joined: {{.User.CreatedAt}}

+

Last Updated: {{.User.UpdatedAt}}

+

Verified: {{.User.Verified}}

+ Edit Profile +
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/profile_edit.html b/templates/pages/profile_edit.html new file mode 100644 index 0000000..80d01f5 --- /dev/null +++ b/templates/pages/profile_edit.html @@ -0,0 +1,29 @@ +{{define "profile_edit"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

Edit Profile

+
+
+
+ +
+ +
+ +
+ +
+
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/signup.html b/templates/pages/signup.html new file mode 100644 index 0000000..480ba05 --- /dev/null +++ b/templates/pages/signup.html @@ -0,0 +1,30 @@ +{{define "signup"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

Sign Up

+
+
+ {{if .Error}} +

{{.Error}}

+ {{end}} +
+ +
+ +
+ +
+
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/thread.html b/templates/pages/thread.html new file mode 100644 index 0000000..4ae367b --- /dev/null +++ b/templates/pages/thread.html @@ -0,0 +1,60 @@ +{{define "thread"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

{{.Thread.Title}}

+ {{if eq .Thread.Type "question"}} + {{if .Thread.AcceptedAnswerPostID}} +

Accepted Answer: Post {{.Thread.AcceptedAnswerPostID}}

+ {{end}} + {{end}} +
+
+ {{range .Posts}} +
+
+

{{.Title}}

+

Posted by User {{.UserID}} on {{.PostTime}}

+ {{if gt .ReplyTo 0}} +

Reply to post {{.ReplyTo}}

+ {{end}} +
+

{{.Content}}

+ {{if $.LoggedIn}} +
+ + + +
+
+ + + +
+ Reply + {{end}} +
+ {{end}} +
+ {{if .LoggedIn}} +
+

Post a Message

+
+ +
+ +
+
+ {{end}} +
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/pages/userhome.html b/templates/pages/userhome.html new file mode 100644 index 0000000..541e431 --- /dev/null +++ b/templates/pages/userhome.html @@ -0,0 +1,21 @@ +{{define "userhome"}} + + + + {{.Title}} + + + + {{template "navbar" .}} +
+
+

Welcome, {{.Username}}

+
+
+

This is your user home page.

+
+
+ {{template "cookie_banner" .}} + + +{{end}} \ No newline at end of file diff --git a/templates/partials/cookie_banner.html b/templates/partials/cookie_banner.html new file mode 100644 index 0000000..b638ba9 --- /dev/null +++ b/templates/partials/cookie_banner.html @@ -0,0 +1,7 @@ +{{define "cookie_banner"}} +{{if .ShowCookieBanner}} + +{{end}} +{{end}} \ No newline at end of file diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html new file mode 100644 index 0000000..0f6af1d --- /dev/null +++ b/templates/partials/navbar.html @@ -0,0 +1,17 @@ +{{define "navbar"}} + +
+{{end}} \ No newline at end of file