Initial Commit
commit
673cb96dff
|
@ -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 <user>@<threadr.ip|no public access set up currently>
|
||||||
|
cd /var/www/git
|
||||||
|
sudo -u www-data -s
|
||||||
|
rm -rf ./web-deployment
|
||||||
|
git clone <ssh git repository link>
|
||||||
|
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
|
||||||
|
"<MACRO>":"<text>" → direct replacement
|
||||||
|
"<MACRO>":["file","<file path>"] → insert file
|
||||||
|
"<MACRO>":["exec","<command>"] → 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.
|
|
@ -0,0 +1,25 @@
|
||||||
|
<p>
|
||||||
|
Hello there! This is the official ThreadR instance provided by the ThreadR development team.
|
||||||
|
</p>
|
||||||
|
<h2>
|
||||||
|
What is ThreadR?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<h2>
|
||||||
|
Who are we?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
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: <a href="https://discord.gg/r3w3zSkEUE"> discord.gg/r3w3zSkEUE </a>
|
||||||
|
</p>
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"domain_name": "threadr.lostcave.ddnss.de",
|
||||||
|
"threadr_dir": "/threadr",
|
||||||
|
"db_username": "root",
|
||||||
|
"db_password": "arnoeomelhor",
|
||||||
|
"db_database": "threadr_test",
|
||||||
|
"db_svr_host": "localhost"
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Lista de frutas</h1>
|
||||||
|
<div id="array"></div>
|
||||||
|
<div id="pessoa"></div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
let frutas = [];
|
||||||
|
frutas.push("Maçã");
|
||||||
|
frutas.push("Laranja");
|
||||||
|
frutas.push("Limão");
|
||||||
|
document.getElementById("array").innerHTML = `Frutas = ${frutas}`;
|
||||||
|
|
||||||
|
let pessoa = [];
|
||||||
|
pessoa = {
|
||||||
|
nome: "Joaquim",
|
||||||
|
idade: 17,
|
||||||
|
salario: 14400
|
||||||
|
}
|
||||||
|
document.getElementById("pessoa").innerHTML = `${pessoa["nome"]}`;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
|
@ -0,0 +1,272 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="261.82941mm"
|
||||||
|
height="95.154205mm"
|
||||||
|
viewBox="0 0 261.82941 95.154203"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||||
|
sodipodi:docname="ThreadR.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient1092">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#a9dfff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop1088" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#a9dfff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop1090" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient1060">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#766fff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop1056" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#766fff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop1058" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1060"
|
||||||
|
id="linearGradient1062"
|
||||||
|
x1="30.859402"
|
||||||
|
y1="95.286171"
|
||||||
|
x2="146.35486"
|
||||||
|
y2="95.286171"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(2.2724332,0,0,0.98143486,-39.444389,5.9391184)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1092"
|
||||||
|
id="linearGradient1094"
|
||||||
|
x1="-144.12688"
|
||||||
|
y1="82.6875"
|
||||||
|
x2="-39.544453"
|
||||||
|
y2="82.6875"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(2.5099298,0,0,0.96280136,68.591637,19.844646)" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.70710678"
|
||||||
|
inkscape:cx="84.893034"
|
||||||
|
inkscape:cy="106.28905"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer4"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:measure-start="0,0"
|
||||||
|
inkscape:measure-end="0,0"
|
||||||
|
inkscape:window-width="1600"
|
||||||
|
inkscape:window-height="847"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="1"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer5"
|
||||||
|
inkscape:label="background"
|
||||||
|
transform="translate(-30.990202,-96.114479)">
|
||||||
|
<rect
|
||||||
|
style="display:inline;opacity:1;fill:#f9f9f9;fill-opacity:1;stroke:none;stroke-width:0.78851396;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1103"
|
||||||
|
width="261.82941"
|
||||||
|
height="95.154205"
|
||||||
|
x="30.990202"
|
||||||
|
y="96.114479" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer3"
|
||||||
|
inkscape:label="window"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-30.990202,-96.114479)">
|
||||||
|
<rect
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.47835484;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1021"
|
||||||
|
width="1.0631751"
|
||||||
|
height="88.470581"
|
||||||
|
x="30.999176"
|
||||||
|
y="102.7981" />
|
||||||
|
<path
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.48693055;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 291.71793,102.7981 v 88.47058 h 1.10168 V 102.7981 Z"
|
||||||
|
id="rect1021-9"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.53601629;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-6-2-9-2"
|
||||||
|
width="259.65558"
|
||||||
|
height="5.0737643"
|
||||||
|
x="32.062351"
|
||||||
|
y="186.19492"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:url(#linearGradient1094);fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1023-6"
|
||||||
|
width="261.82043"
|
||||||
|
height="6.6836219"
|
||||||
|
x="-292.81961"
|
||||||
|
y="96.114479"
|
||||||
|
transform="scale(-1,1)" />
|
||||||
|
<rect
|
||||||
|
style="display:inline;opacity:1;fill:url(#linearGradient1062);fill-opacity:1;stroke:none;stroke-width:0.53430772;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1023"
|
||||||
|
width="261.82043"
|
||||||
|
height="6.6836276"
|
||||||
|
x="30.999176"
|
||||||
|
y="96.114479" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:label="contents"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-30.990202,-96.114479)">
|
||||||
|
<path
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.46272928;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 33.365806,121.08017 4.989638,-12.31832 c 1.088722,-2.58288 1.758941,-2.58288 5.171142,-2.58288 H 111.3019 c 3.41221,0 4.08244,0 5.17115,2.58288 l 4.98963,12.31833 z"
|
||||||
|
id="path817"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_CYAN%;fill-opacity:1;stroke:none;stroke-width:0.46806985;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909"
|
||||||
|
width="255.74254"
|
||||||
|
height="15.640329"
|
||||||
|
x="32.062351"
|
||||||
|
y="121.10081"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_ORANGE%;fill-opacity:1;stroke:none;stroke-width:0.41052076;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-5"
|
||||||
|
width="255.74254"
|
||||||
|
height="9.9996996"
|
||||||
|
x="32.062351"
|
||||||
|
y="136.74113"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_PINK%;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-6-2-9"
|
||||||
|
width="255.74255"
|
||||||
|
height="10.471648"
|
||||||
|
x="32.062351"
|
||||||
|
y="175.72327"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_BEIGE%;fill-opacity:1;stroke:none;stroke-width:0.52205408;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-6-2-1"
|
||||||
|
width="255.74254"
|
||||||
|
height="28.982441"
|
||||||
|
x="32.062351"
|
||||||
|
y="146.74083"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ececec;stroke:none;stroke-width:0.48659384;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect983"
|
||||||
|
width="3.9130244"
|
||||||
|
height="65.094109"
|
||||||
|
x="287.8049"
|
||||||
|
y="121.10081" />
|
||||||
|
<rect
|
||||||
|
style="fill:#b3b3b3;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect983-7"
|
||||||
|
width="2.182373"
|
||||||
|
height="45.629673"
|
||||||
|
x="288.67023"
|
||||||
|
y="127.22635"
|
||||||
|
ry="1.1820796"
|
||||||
|
rx="1.0911865" />
|
||||||
|
<rect
|
||||||
|
style="fill:#b3b3b3;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1002"
|
||||||
|
width="0.46817124"
|
||||||
|
height="0.48868293"
|
||||||
|
x="144.64522"
|
||||||
|
y="120.98614"
|
||||||
|
rx="1.5645972"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:#b3b3b3;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1002-0"
|
||||||
|
width="0.46817127"
|
||||||
|
height="0.48868293"
|
||||||
|
x="141.20036"
|
||||||
|
y="120.98614"
|
||||||
|
rx="1.5645972"
|
||||||
|
ry="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="ThreadR"
|
||||||
|
transform="translate(4.9926758e-8,-4.1053391)">
|
||||||
|
<g
|
||||||
|
aria-label="ThreadR"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:98.77777863px;line-height:1.25;font-family:Playball;-inkscape-font-specification:Playball;letter-spacing:0px;word-spacing:0px;fill:%COLOR_BLUE%;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||||
|
id="text840">
|
||||||
|
<path
|
||||||
|
d="M 66.388955,26.034411 52.461289,25.441745 q -0.987778,0 -6.519334,11.853333 -5.531555,11.754556 -9.581444,22.817667 -4.049889,11.063111 -4.049889,16.002 0,4.840111 3.062111,4.840111 3.160889,0 7.902222,-6.420555 0.493889,-0.592667 0.987778,-0.592667 0.493889,0 0.493889,0.889 0,0.889 -1.382889,3.259667 -1.284111,2.370666 -3.457222,5.136444 -2.173111,2.667 -5.630334,4.741334 -3.457222,2.173111 -7.112,2.173111 -5.432777,0 -5.432777,-7.507111 0,-9.285112 9.383888,-31.60889 6.716889,-15.705666 12.347223,-26.077333 H 42.1884 L 32.014288,24.7503 q -8.494888,0 -14.816666,2.173111 -3.457223,1.185334 -5.531556,3.556 -1.9755555,2.271889 -1.9755555,5.531556 l 0.7902225,4.148667 q 0,0.987778 -0.987778,0.987778 -1.2841112,0 -2.0743334,-2.469445 -0.7902222,-2.568222 -0.7902222,-4.445 0,-4.642556 2.1731111,-7.902222 2.1731115,-3.259667 5.9266665,-4.938889 7.112,-3.160889 15.705667,-3.160889 8.593667,0 19.459222,1.086555 10.865556,0.987778 19.755556,0.987778 8.89,0 11.557,-2.765778 0.296334,0.691445 0.296334,1.382889 0,3.259667 -4.148667,5.235223 -4.148667,1.876777 -10.964334,1.876777 z"
|
||||||
|
style="fill:%COLOR_BLUE%;fill-opacity:1;stroke-width:0.26458332"
|
||||||
|
id="path841" />
|
||||||
|
<path
|
||||||
|
d="m 83.240932,73.360961 q -8.938683,15.043151 -15.115823,15.043151 -4.287661,0 -4.287661,-4.941711 0,-5.741106 4.57835,-16.351251 1.017412,-2.688873 1.017412,-3.706284 0,-1.453444 -1.308101,-1.453444 -0.872066,0 -2.979561,1.017411 -2.034822,1.017411 -3.9243,2.979561 -1.889478,1.96215 -3.197578,4.57835 -1.235428,2.543528 -2.034822,4.433006 -0.726723,1.889478 -1.526117,4.651023 -0.799394,2.688872 -1.453445,4.433005 -0.581377,1.671462 -1.235427,3.924301 h -7.049206 q 1.96215,-5.014384 5.595761,-15.98789 3.706284,-11.046178 6.322484,-17.29599 2.6162,-6.322484 5.087056,-10.610145 2.543528,-4.360333 5.741105,-7.267222 3.270251,-2.906889 6.83119,-2.906889 3.633611,0 3.633611,3.415594 0,2.398184 -1.96215,6.322484 -4.214989,8.284634 -14.098412,19.185467 3.9243,-2.834216 6.177139,-3.706283 2.325511,-0.872067 4.433006,-0.872067 4.142317,0 4.142317,3.633611 0,1.453445 -0.508706,2.688873 -0.508705,1.235428 -1.162755,2.834217 -0.581378,1.598789 -1.017411,2.6162 -0.363362,1.017411 -1.017412,2.761544 -0.65405,1.744134 -1.017411,2.979562 -0.944739,3.052233 -0.944739,4.869039 0,1.816805 1.235428,1.816805 4.360334,0 10.900834,-12.644967 0.218017,-0.508705 0.508706,-0.508705 0.363361,0 0.363361,1.380772 0,1.3081 -0.726723,2.688872 z M 74.447593,35.716748 q -2.543528,0 -6.685845,8.066617 -4.069644,8.066617 -7.993944,18.531418 9.956094,-11.700229 13.589706,-19.185468 2.034822,-4.57835 2.034822,-5.959122 0,-1.453445 -0.944739,-1.453445 z"
|
||||||
|
style="font-size:72.67222595px;fill:%COLOR_BLUE%;fill-opacity:1"
|
||||||
|
id="path843" />
|
||||||
|
<path
|
||||||
|
d="m 100.06455,58.753844 3.70629,0.07267 h 0.72672 q -1.09009,0.799394 -3.12491,4.723695 -1.962148,3.9243 -2.979559,5.959122 -0.944739,1.96215 -2.180167,5.377745 -1.162756,3.342922 -1.162756,5.450417 0,2.107494 1.453445,2.107494 0.944739,-0.07267 2.6162,-1.3081 1.744137,-1.3081 2.834217,-2.688872 1.16276,-1.453445 2.47086,-3.633611 1.38077,-2.252839 2.32551,-3.851628 1.01741,-1.671461 1.16275,-1.671461 0.50871,0 0.50871,1.380772 0,1.3081 -0.72672,2.543528 -3.12491,5.159728 -5.15973,7.921272 -2.03482,2.761545 -4.941714,5.087056 -2.834217,2.252839 -5.159728,2.252839 -4.142317,0 -4.142317,-6.104467 0,-4.142317 1.598789,-9.374717 1.598789,-5.2324 4.723695,-10.101439 -2.543528,2.034822 -5.087056,2.034822 -0.799394,0 -1.671461,-0.363361 -5.159728,10.174111 -5.595762,10.174111 -0.363361,0 -0.363361,-1.162755 0,-1.235428 0.872067,-3.342923 0.944739,-2.180166 1.96215,-4.142316 1.090084,-1.96215 1.162756,-2.180167 -2.6162,-1.3081 -2.6162,-4.214989 0,-1.453445 0.872066,-2.6162 0.944739,-1.162756 2.543528,-1.162756 1.598789,0 2.6162,1.017411 -0.145344,0.436033 -0.145344,1.3081 0,1.96215 1.380772,3.124906 1.453445,1.090083 3.488267,1.090083 0.581378,0 0.872067,-0.07267 2.543528,-3.633611 5.159726,-3.633611 z"
|
||||||
|
style="font-size:72.67222595px;fill:%COLOR_BLUE%;fill-opacity:1"
|
||||||
|
id="path845" />
|
||||||
|
<path
|
||||||
|
d="m 134.07062,69.0733 q 0.43603,0 0.43603,1.453444 0,1.380773 -1.16276,3.342923 -1.16275,1.96215 -2.32551,3.706283 -1.09008,1.744134 -2.97956,4.069645 -1.88948,2.325511 -3.70628,3.706283 -4.57835,3.342923 -9.22938,3.342923 -4.57835,0 -7.41256,-2.325511 -2.83422,-2.398184 -2.83422,-7.267223 0,-7.412567 5.45042,-13.735051 5.45041,-6.322483 12.71764,-6.322483 3.27025,0 5.2324,1.744133 1.96215,1.671461 1.96215,4.069645 0,4.069644 -3.85163,6.5405 -3.85163,2.470856 -9.08403,2.470856 -3.05223,0 -4.50568,-0.799395 -1.38077,3.706284 -1.38077,6.540501 0,6.104467 5.15973,6.104467 3.05223,0 6.24981,-2.325512 3.27025,-2.398183 5.52309,-5.305072 2.32551,-2.979561 5.2324,-8.502651 0.29069,-0.508705 0.50871,-0.508705 z m -7.70326,-5.741106 q 0,-2.107495 -2.03482,-2.107495 -2.83422,0 -6.32249,3.488267 -3.48826,3.488267 -3.48826,5.668434 0,1.453444 2.76154,1.453444 2.76155,0 5.74111,-2.688872 3.05223,-2.688872 3.34292,-5.813778 z"
|
||||||
|
style="font-size:72.67222595px;fill:%COLOR_BLUE%;fill-opacity:1"
|
||||||
|
id="path847" />
|
||||||
|
<path
|
||||||
|
d="m 160.69931,58.899188 3.63361,0.07267 h 0.72672 q -2.39818,1.962151 -5.88645,9.956095 -3.48827,7.921273 -3.48827,11.554884 0,1.889478 1.3081,1.889478 4.21499,0 10.75549,-12.42695 0.29069,-0.508706 0.50871,-0.508706 0.43603,0 0.43603,1.380772 0,1.3081 -0.72672,2.543528 -8.93869,15.043151 -14.97048,15.043151 -3.9243,0 -3.9243,-6.031795 0,-4.505678 1.52612,-8.866011 -8.28464,14.679789 -15.33384,14.679789 -4.86904,0 -4.86904,-6.685844 0,-5.087056 2.18016,-10.319457 2.25284,-5.2324 6.39516,-8.866011 4.14232,-3.706284 9.08403,-3.706284 2.32551,0 4.36033,1.090084 2.03482,1.017411 2.68887,3.342922 0.7994,-1.671461 2.47086,-2.906889 1.67146,-1.235428 3.12491,-1.235428 z m -6.0318,4.796367 q -0.72672,-2.325511 -3.41559,-2.325511 -4.79637,0 -9.30205,6.685845 -4.50568,6.613172 -4.50568,11.845573 0,2.252839 1.74414,2.252839 1.88947,0 4.433,-2.325512 2.54353,-2.398183 4.57835,-5.595761 4.50568,-7.121878 6.46783,-10.537473 z"
|
||||||
|
style="font-size:72.67222595px;fill:%COLOR_BLUE%;fill-opacity:1"
|
||||||
|
id="path849" />
|
||||||
|
<path
|
||||||
|
d="m 206.38516,40.440443 q -2.39819,1.744133 -6.32249,9.81075 -3.85162,7.993945 -6.83119,16.932629 -2.97956,8.866012 -2.97956,12.862984 0,2.325511 1.23543,2.325511 1.23543,0 3.05223,-1.598789 1.88948,-1.598789 2.68888,-2.688872 0.79939,-1.090084 2.10749,-3.27025 1.38077,-2.252839 2.32551,-3.851628 1.01741,-1.671461 1.16276,-1.671461 0.5087,0 0.5087,1.380772 -0.0727,2.543528 -6.03179,10.610145 -1.96215,2.6162 -4.7237,4.869039 -2.76154,2.252839 -5.01438,2.252839 -3.85163,0 -3.85163,-5.741106 0,-4.214989 2.1075,-10.828162 -3.34293,6.613173 -7.63059,11.482212 -4.28766,4.869039 -8.57532,4.869039 -4.86904,0 -4.86904,-6.685844 0,-5.087056 2.18017,-10.319457 2.25284,-5.2324 6.39515,-8.866011 4.14232,-3.706284 8.93869,-3.706284 4.86904,0 6.68584,3.197578 2.03482,-3.996972 4.14232,-9.229372 2.18017,-5.232401 2.83422,-6.758517 0.72672,-1.598789 1.01741,-2.325512 0.36336,-0.726722 0.87206,-1.380772 0.7994,-1.017411 1.88948,-1.235428 1.16276,-0.290689 3.63361,-0.290689 2.54353,-0.07267 3.05224,-0.145344 z m -17.8047,22.237701 q -0.87207,-1.3081 -2.90689,-1.3081 -3.27025,0 -6.61317,3.342922 -3.27025,3.27025 -5.30507,7.630584 -1.96215,4.287661 -1.96215,7.557912 0,2.252839 1.74413,2.252839 5.88645,0 15.04315,-19.476157 z"
|
||||||
|
style="font-size:72.67222595px;fill:%COLOR_BLUE%;fill-opacity:1"
|
||||||
|
id="path851" />
|
||||||
|
<path
|
||||||
|
d="m 250.30984,40.077082 q 3.41559,2.761544 3.41559,7.049206 0,4.287661 -2.54352,7.557911 -4.50568,5.959123 -13.8804,7.703256 l -2.32551,0.436033 q 3.63361,12.644968 15.26117,24.999246 -1.09009,0.218017 -3.34292,0.218017 l -5.59577,-0.218017 q -2.90688,0 -6.17714,-5.668433 -3.19757,-5.741106 -5.08705,-11.627557 -1.88948,-5.959122 -1.88948,-7.921272 0,-1.090084 1.01741,-1.162756 17.07798,-2.107494 17.58668,-13.371689 0,-3.996973 -2.39818,-6.17714 -2.32551,-2.180166 -7.70326,-2.470855 -5.66843,7.630583 -12.57229,24.417868 -6.83119,17.077973 -6.83119,22.52839 0,1.090083 0.29069,1.598789 h -9.37472 q 2.03482,-8.79334 8.57532,-24.417868 8.72067,-20.784257 11.2642,-23.618474 -6.17714,0.145345 -12.5723,2.543528 -6.32248,2.325511 -7.33989,5.595762 -0.0727,0.218016 -0.29069,0.218016 -0.65405,0 -0.65405,-0.944739 0,-0.145344 0.14534,-0.726722 1.38077,-3.996972 3.99697,-6.177139 2.6162,-2.180167 5.08706,-2.688873 2.54353,-0.508705 6.24981,-0.508705 h 17.65935 q 6.68585,0 10.02877,2.834217 z"
|
||||||
|
style="font-size:72.67222595px;fill:%COLOR_BLUE%;fill-opacity:1"
|
||||||
|
id="path853" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,249 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="97.333481mm"
|
||||||
|
height="95.154205mm"
|
||||||
|
viewBox="0 0 97.333492 95.154203"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||||
|
sodipodi:docname="ThreadR_Home.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient1092">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#a9dfff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop1088" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#a9dfff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop1090" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="linearGradient1060">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#766fff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop1056" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#766fff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop1058" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1060"
|
||||||
|
id="linearGradient1062"
|
||||||
|
x1="30.859402"
|
||||||
|
y1="95.286171"
|
||||||
|
x2="146.35486"
|
||||||
|
y2="95.286171"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.84479197,0,0,0.98143437,4.8113177,5.9391634)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1092"
|
||||||
|
id="linearGradient1094"
|
||||||
|
x1="-144.12688"
|
||||||
|
y1="82.6875"
|
||||||
|
x2="-39.544453"
|
||||||
|
y2="82.6875"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.93308299,0,0,0.96280088,6.0243672,19.844684)" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="207.39258"
|
||||||
|
inkscape:cy="-260.45274"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer3"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:measure-start="0,0"
|
||||||
|
inkscape:measure-end="0,0"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-height="997"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="1"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer5"
|
||||||
|
inkscape:label="background"
|
||||||
|
transform="translate(-30.999176,-96.114479)">
|
||||||
|
<rect
|
||||||
|
style="display:inline;opacity:1;fill:#f9f9f9;fill-opacity:1;stroke:none;stroke-width:0.48078543;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1103"
|
||||||
|
width="97.342438"
|
||||||
|
height="95.154205"
|
||||||
|
x="30.990202"
|
||||||
|
y="96.114479" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer3"
|
||||||
|
inkscape:label="window"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-30.999176,-96.114479)">
|
||||||
|
<rect
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.47835484;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1021"
|
||||||
|
width="1.0631751"
|
||||||
|
height="88.470581"
|
||||||
|
x="30.999176"
|
||||||
|
y="102.7981" />
|
||||||
|
<path
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.48693055;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 127.23097,102.7981 v 88.47058 h 1.10168 V 102.7981 Z"
|
||||||
|
id="rect1021-9"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.32450849;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-6-2-9-2"
|
||||||
|
width="95.168625"
|
||||||
|
height="5.0737643"
|
||||||
|
x="32.062351"
|
||||||
|
y="186.19492"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:url(#linearGradient1094);fill-opacity:1;stroke:none;stroke-width:0.32264262;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1023-6"
|
||||||
|
width="97.333473"
|
||||||
|
height="6.6836185"
|
||||||
|
x="-128.33266"
|
||||||
|
y="96.114479"
|
||||||
|
transform="scale(-1,1)" />
|
||||||
|
<rect
|
||||||
|
style="display:inline;opacity:1;fill:url(#linearGradient1062);fill-opacity:1;stroke:none;stroke-width:0.32577717;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1023"
|
||||||
|
width="97.333466"
|
||||||
|
height="6.6836243"
|
||||||
|
x="30.999176"
|
||||||
|
y="96.114479" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:label="contents"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-30.999176,-96.114479)">
|
||||||
|
<path
|
||||||
|
style="fill:#e6e6e6;stroke:none;stroke-width:0.46272928;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 33.365806,121.08017 4.989638,-12.31832 c 1.088722,-2.58288 1.758941,-2.58288 5.171142,-2.58288 H 111.3019 c 3.41221,0 4.08244,0 5.17115,2.58288 l 4.98963,12.31833 z"
|
||||||
|
id="path817"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_CYAN%;fill-opacity:1;stroke:none;stroke-width:0.27960116;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909"
|
||||||
|
width="91.2556"
|
||||||
|
height="15.640329"
|
||||||
|
x="32.062351"
|
||||||
|
y="121.10081"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_ORANGE%;fill-opacity:1;stroke:none;stroke-width:0.24522424;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-5"
|
||||||
|
width="91.255592"
|
||||||
|
height="9.9996996"
|
||||||
|
x="32.062351"
|
||||||
|
y="136.74113"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_PINK%;fill-opacity:1;stroke:none;stroke-width:0.31609729;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-6-2-9"
|
||||||
|
width="91.2556"
|
||||||
|
height="10.471648"
|
||||||
|
x="32.062351"
|
||||||
|
y="175.72327"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:%COLOR_BEIGE%;fill-opacity:1;stroke:none;stroke-width:0.31184855;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect909-6-2-1"
|
||||||
|
width="91.255592"
|
||||||
|
height="28.982441"
|
||||||
|
x="32.062351"
|
||||||
|
y="146.74083"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ececec;stroke:none;stroke-width:0.48659384;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect983"
|
||||||
|
width="3.9130244"
|
||||||
|
height="65.094109"
|
||||||
|
x="123.31795"
|
||||||
|
y="121.10081" />
|
||||||
|
<rect
|
||||||
|
style="fill:#b3b3b3;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect983-7"
|
||||||
|
width="2.182373"
|
||||||
|
height="45.629673"
|
||||||
|
x="124.17254"
|
||||||
|
y="125.91673"
|
||||||
|
ry="1.1820796"
|
||||||
|
rx="1.0911865" />
|
||||||
|
<rect
|
||||||
|
style="fill:#b3b3b3;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1002"
|
||||||
|
width="0.46817124"
|
||||||
|
height="0.48868293"
|
||||||
|
x="144.64522"
|
||||||
|
y="120.98614"
|
||||||
|
rx="1.5645972"
|
||||||
|
ry="0" />
|
||||||
|
<rect
|
||||||
|
style="fill:#b3b3b3;stroke:none;stroke-width:0.5291667;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1002-0"
|
||||||
|
width="0.46817127"
|
||||||
|
height="0.48868293"
|
||||||
|
x="141.20036"
|
||||||
|
y="120.98614"
|
||||||
|
rx="1.5645972"
|
||||||
|
ry="0" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="ThreadR"
|
||||||
|
transform="translate(-0.00897398,-4.1053392)">
|
||||||
|
<g
|
||||||
|
aria-label="ThreadR"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:98.77777863px;line-height:1.25;font-family:Playball;-inkscape-font-specification:Playball;letter-spacing:0px;word-spacing:0px;fill:%COLOR_BLUE%;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||||
|
id="text840">
|
||||||
|
<path
|
||||||
|
d="M 66.388955,26.034411 52.461289,25.441745 q -0.987778,0 -6.519334,11.853333 -5.531555,11.754556 -9.581444,22.817667 -4.049889,11.063111 -4.049889,16.002 0,4.840111 3.062111,4.840111 3.160889,0 7.902222,-6.420555 0.493889,-0.592667 0.987778,-0.592667 0.493889,0 0.493889,0.889 0,0.889 -1.382889,3.259667 -1.284111,2.370666 -3.457222,5.136444 -2.173111,2.667 -5.630334,4.741334 -3.457222,2.173111 -7.112,2.173111 -5.432777,0 -5.432777,-7.507111 0,-9.285112 9.383888,-31.60889 6.716889,-15.705666 12.347223,-26.077333 H 42.1884 L 32.014288,24.7503 q -8.494888,0 -14.816666,2.173111 -3.457223,1.185334 -5.531556,3.556 -1.9755555,2.271889 -1.9755555,5.531556 l 0.7902225,4.148667 q 0,0.987778 -0.987778,0.987778 -1.2841112,0 -2.0743334,-2.469445 -0.7902222,-2.568222 -0.7902222,-4.445 0,-4.642556 2.1731111,-7.902222 2.1731115,-3.259667 5.9266665,-4.938889 7.112,-3.160889 15.705667,-3.160889 8.593667,0 19.459222,1.086555 10.865556,0.987778 19.755556,0.987778 8.89,0 11.557,-2.765778 0.296334,0.691445 0.296334,1.382889 0,3.259667 -4.148667,5.235223 -4.148667,1.876777 -10.964334,1.876777 z"
|
||||||
|
style="fill:%COLOR_BLUE%;fill-opacity:1;stroke-width:0.26458332"
|
||||||
|
id="path841"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 752 B |
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{{define "base"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
{{block "content" .}}{{end}} <!-- Define a block for content -->
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{define "about"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>About ThreadR</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
{{.AboutContent}}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,42 @@
|
||||||
|
{{define "board"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>{{.Board.Name}}</h2>
|
||||||
|
<p>{{.Board.Description}}</p>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<ul>
|
||||||
|
{{range .Threads}}
|
||||||
|
<li><a href="{{$.BasePath}}/thread/?id={{.ID}}">{{.Title}}</a> ({{.Type}})</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{{if .LoggedIn}}
|
||||||
|
<section>
|
||||||
|
<h3>Create New Thread</h3>
|
||||||
|
<form method="post" action="{{.BasePath}}/board/?id={{.Board.ID}}&action=create_thread">
|
||||||
|
<label for="title">Thread Title:</label>
|
||||||
|
<input type="text" id="title" name="title" required><br>
|
||||||
|
<label for="type">Thread Type:</label>
|
||||||
|
<select id="type" name="type">
|
||||||
|
<option value="classic">Classic</option>
|
||||||
|
<option value="chat">Chat</option>
|
||||||
|
<option value="question">Question</option>
|
||||||
|
</select><br>
|
||||||
|
<input type="submit" value="Create Thread">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,36 @@
|
||||||
|
{{define "boards"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>Boards</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<h3>Public Boards</h3>
|
||||||
|
<ul>
|
||||||
|
{{range .PublicBoards}}
|
||||||
|
<li><a href="{{$.BasePath}}/board/?id={{.ID}}">{{.Name}}</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{{if .LoggedIn}}
|
||||||
|
<section>
|
||||||
|
<h3>Private Boards</h3>
|
||||||
|
<ul>
|
||||||
|
{{range .PrivateBoards}}
|
||||||
|
<li><a href="{{$.BasePath}}/board/?id={{.ID}}">{{.Name}}</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{define "home"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1><center>Welcome to ThreadR</center></h1>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<img src="{{.StaticPath}}/img/ThreadR.png" alt="ThreadR" height="100%" width="100%">
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{define "login"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>Login</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
{{if .Error}}
|
||||||
|
<p style="color: red;">{{.Error}}</p>
|
||||||
|
{{end}}
|
||||||
|
<form method="post" action="{{.BasePath}}/login/">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required><br>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required><br>
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,25 @@
|
||||||
|
{{define "news"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>News</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<ul>
|
||||||
|
{{range .News}}
|
||||||
|
<li>{{.}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{define "profile"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>Profile</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<p>Username: {{.User.Username}}</p>
|
||||||
|
<p>Display Name: {{.DisplayName}}</p>
|
||||||
|
{{if .User.PfpURL}}
|
||||||
|
<img src="{{.User.PfpURL}}" alt="Profile Picture">
|
||||||
|
{{end}}
|
||||||
|
<p>Bio: {{.User.Bio}}</p>
|
||||||
|
<p>Joined: {{.User.CreatedAt}}</p>
|
||||||
|
<p>Last Updated: {{.User.UpdatedAt}}</p>
|
||||||
|
<p>Verified: {{.User.Verified}}</p>
|
||||||
|
<a href="{{.BasePath}}/profile/edit/">Edit Profile</a>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{{define "profile_edit"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>Edit Profile</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<form method="post" action="{{.BasePath}}/profile/edit/">
|
||||||
|
<label for="display_name">Display Name:</label>
|
||||||
|
<input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}"><br>
|
||||||
|
<label for="pfp_url">Profile Picture URL:</label>
|
||||||
|
<input type="text" id="pfp_url" name="pfp_url" value="{{.User.PfpURL}}"><br>
|
||||||
|
<label for="bio">Bio:</label>
|
||||||
|
<textarea id="bio" name="bio">{{.User.Bio}}</textarea><br>
|
||||||
|
<input type="submit" value="Save">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{define "signup"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
{{if .Error}}
|
||||||
|
<p style="color: red;">{{.Error}}</p>
|
||||||
|
{{end}}
|
||||||
|
<form method="post" action="{{.BasePath}}/signup/">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required><br>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required><br>
|
||||||
|
<input type="submit" value="Sign Up">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,60 @@
|
||||||
|
{{define "thread"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>{{.Thread.Title}}</h2>
|
||||||
|
{{if eq .Thread.Type "question"}}
|
||||||
|
{{if .Thread.AcceptedAnswerPostID}}
|
||||||
|
<p>Accepted Answer: <a href="#{{.Thread.AcceptedAnswerPostID}}">Post {{.Thread.AcceptedAnswerPostID}}</a></p>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
{{range .Posts}}
|
||||||
|
<article id="{{.ID}}">
|
||||||
|
<header>
|
||||||
|
<h3>{{.Title}}</h3>
|
||||||
|
<p>Posted by User {{.UserID}} on {{.PostTime}}</p>
|
||||||
|
{{if gt .ReplyTo 0}}
|
||||||
|
<p>Reply to post {{.ReplyTo}}</p>
|
||||||
|
{{end}}
|
||||||
|
</header>
|
||||||
|
<p>{{.Content}}</p>
|
||||||
|
{{if $.LoggedIn}}
|
||||||
|
<form method="post" action="{{$.BasePath}}/like/" style="display:inline;">
|
||||||
|
<input type="hidden" name="post_id" value="{{.ID}}">
|
||||||
|
<input type="hidden" name="type" value="like">
|
||||||
|
<button type="submit">Like</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="{{$.BasePath}}/like/" style="display:inline;">
|
||||||
|
<input type="hidden" name="post_id" value="{{.ID}}">
|
||||||
|
<input type="hidden" name="type" value="dislike">
|
||||||
|
<button type="submit">Dislike</button>
|
||||||
|
</form>
|
||||||
|
<a href="{{$.BasePath}}/thread/?id={{$.Thread.ID}}&action=submit&to={{.ID}}">Reply</a>
|
||||||
|
{{end}}
|
||||||
|
</article>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{if .LoggedIn}}
|
||||||
|
<section>
|
||||||
|
<h3>Post a Message</h3>
|
||||||
|
<form method="post" action="{{.BasePath}}/thread/?id={{.Thread.ID}}&action=submit">
|
||||||
|
<label for="content">Content:</label>
|
||||||
|
<textarea id="content" name="content" required></textarea><br>
|
||||||
|
<input type="submit" value="Post">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{define "userhome"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>Welcome, {{.Username}}</h2>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<p>This is your user home page.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{{define "cookie_banner"}}
|
||||||
|
{{if .ShowCookieBanner}}
|
||||||
|
<div class="banner">
|
||||||
|
<p>We use cookies to enhance your experience. <a href="{{.BasePath}}/accept_cookie/?from={{.CurrentURL | urlquery}}">Accept</a></p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{define "navbar"}}
|
||||||
|
<ul class="topnav">
|
||||||
|
<li><a {{if eq .Navbar "home"}}class="active"{{end}} href="{{.BasePath}}/">Home</a></li>
|
||||||
|
{{if .LoggedIn}}
|
||||||
|
<li><a {{if eq .Navbar "userhome"}}class="active"{{end}} href="{{.BasePath}}/userhome/">User Home</a></li>
|
||||||
|
<li><a {{if eq .Navbar "profile"}}class="active"{{end}} href="{{.BasePath}}/profile/">Profile</a></li>
|
||||||
|
<li><a href="{{.BasePath}}/logout/">Logout</a></li>
|
||||||
|
{{else}}
|
||||||
|
<li><a {{if eq .Navbar "login"}}class="active"{{end}} href="{{.BasePath}}/login/">Login</a></li>
|
||||||
|
<li><a {{if eq .Navbar "signup"}}class="active"{{end}} href="{{.BasePath}}/signup/">Sign Up</a></li>
|
||||||
|
{{end}}
|
||||||
|
<li><a {{if eq .Navbar "boards"}}class="active"{{end}} href="{{.BasePath}}/boards/">Boards</a></li>
|
||||||
|
<li><a {{if eq .Navbar "news"}}class="active"{{end}} href="{{.BasePath}}/news/">News</a></li>
|
||||||
|
<li><a {{if eq .Navbar "about"}}class="active"{{end}} href="{{.BasePath}}/about/">About</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="topnav"></div>
|
||||||
|
{{end}}
|
Loading…
Reference in New Issue