initial commit
continuous-integration/drone Build is failing
Details
continuous-integration/drone Build is failing
Details
This commit is contained in:
parent
27aab70386
commit
fe22956d5d
|
@ -0,0 +1,4 @@
|
||||||
|
.env
|
||||||
|
hatecomputers.club
|
||||||
|
Dockerfile
|
||||||
|
*.db
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build and publish docker image
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: docker
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: gitea_packpub_username
|
||||||
|
password:
|
||||||
|
from_secret: gitea_packpub_password
|
||||||
|
repo: git.hatecomputers.club/hatecomputers/hatecomputers.club
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- main
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
|
@ -0,0 +1,2 @@
|
||||||
|
CLOUDFLARE_TOKEN=
|
||||||
|
CLOUDFLARE_ZONE=
|
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
hatecomputers.club
|
||||||
|
*.db
|
|
@ -5,10 +5,10 @@ WORKDIR /app
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY *.go ./
|
COPY . .
|
||||||
|
|
||||||
RUN go build -o /app/hatecomputers
|
RUN go build -o /app/hatecomputers
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["/app/hatecomputers", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static"]
|
CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static"]
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestContext struct {
|
||||||
|
DBConn *sql.DB
|
||||||
|
Args *args.Arguments
|
||||||
|
|
||||||
|
Id string
|
||||||
|
Start time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain
|
||||||
|
type ContinuationChain func(Continuation, Continuation) ContinuationChain
|
||||||
|
|
||||||
|
func randomId() string {
|
||||||
|
uuid := make([]byte, 16)
|
||||||
|
_, err := rand.Read(uuid)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid[8] = uuid[8]&^0xc0 | 0x80
|
||||||
|
uuid[6] = uuid[6]&^0xf0 | 0x40
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogRequestContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
|
return func(success Continuation, _failure Continuation) ContinuationChain {
|
||||||
|
context.Start = time.Now()
|
||||||
|
context.Id = randomId()
|
||||||
|
|
||||||
|
log.Println(req.Method, req.URL.Path, req.RemoteAddr, context.Id)
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogExecutionTimeContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
|
return func(success Continuation, _failure Continuation) ContinuationChain {
|
||||||
|
end := time.Now()
|
||||||
|
|
||||||
|
log.Println(context.Id, "took", end.Sub(context.Start))
|
||||||
|
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HealthCheckContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
|
return func(success Continuation, _failure Continuation) ContinuationChain {
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
resp.Write([]byte("healthy"))
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FailurePassingContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
|
return func(_success Continuation, failure Continuation) ContinuationChain {
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IdContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
|
return func(success Continuation, _failure Continuation) ContinuationChain {
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
fileServer := http.FileServer(http.Dir(argv.StaticPath))
|
||||||
|
mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
|
||||||
|
|
||||||
|
makeRequestContext := func() *RequestContext {
|
||||||
|
return &RequestContext{
|
||||||
|
DBConn: dbConn,
|
||||||
|
Args: argv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestContext := makeRequestContext()
|
||||||
|
LogRequestContinuation(requestContext, r, w)(TemplateContinuation("home.html", nil, true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestContext := makeRequestContext()
|
||||||
|
LogRequestContinuation(requestContext, r, w)(HealthCheckContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("GET /{name}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestContext := makeRequestContext()
|
||||||
|
name := r.PathValue("name")
|
||||||
|
LogRequestContinuation(requestContext, r, w)(TemplateContinuation(name+".html", nil, true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &http.Server{
|
||||||
|
Addr: ":" + fmt.Sprint(argv.Port),
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func renderTemplate(context *RequestContext, templateName string, showBaseHtml bool, data interface{}) (bytes.Buffer, error) {
|
||||||
|
templatePath := context.Args.TemplatePath
|
||||||
|
basePath := templatePath + "/base_empty.html"
|
||||||
|
if showBaseHtml {
|
||||||
|
basePath = templatePath + "/base.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
templateLocation := templatePath + "/" + templateName
|
||||||
|
tmpl, err := template.New("").ParseFiles(templateLocation, basePath)
|
||||||
|
if err != nil {
|
||||||
|
return bytes.Buffer{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err = tmpl.ExecuteTemplate(&buffer, "base", data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return bytes.Buffer{}, err
|
||||||
|
}
|
||||||
|
return buffer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TemplateContinuation(path string, data interface{}, showBase bool) Continuation {
|
||||||
|
return func(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
||||||
|
html, err := renderTemplate(context, path, true, data)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
html, err = renderTemplate(context, "404.html", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error rendering 404 template", err)
|
||||||
|
resp.WriteHeader(500)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Header().Set("Content-Type", "text/html")
|
||||||
|
resp.Write(html.Bytes())
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error rendering template", err)
|
||||||
|
resp.WriteHeader(500)
|
||||||
|
resp.Write([]byte("error rendering template"))
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
resp.Header().Set("Content-Type", "text/html")
|
||||||
|
resp.Write(html.Bytes())
|
||||||
|
return success(context, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package args
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Arguments struct {
|
||||||
|
DatabasePath string
|
||||||
|
TemplatePath string
|
||||||
|
StaticPath string
|
||||||
|
CloudflareToken string
|
||||||
|
CloudflareZone string
|
||||||
|
Port int
|
||||||
|
Server bool
|
||||||
|
Migrate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetArgs() (*Arguments, error) {
|
||||||
|
port := flag.Int("port", 8080, "Port to listen on")
|
||||||
|
databasePath := flag.String("database-path", "./hatecomputers.db", "Path to the SQLite database")
|
||||||
|
templatePath := flag.String("template-path", "./templates", "Path to the template directory")
|
||||||
|
staticPath := flag.String("static-path", "./static", "Path to the static directory")
|
||||||
|
|
||||||
|
server := flag.Bool("server", false, "Run the server")
|
||||||
|
migrate := flag.Bool("migrate", false, "Run the migrations")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN")
|
||||||
|
cloudflareZone := os.Getenv("CLOUDFLARE_ZONE")
|
||||||
|
|
||||||
|
if cloudflareToken == "" {
|
||||||
|
return nil, errors.New("please set the CLOUDFLARE_TOKEN environment variable")
|
||||||
|
}
|
||||||
|
if cloudflareZone == "" {
|
||||||
|
return nil, errors.New("please set the CLOUDFLARE_ZONE environment variable")
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments := &Arguments{
|
||||||
|
DatabasePath: *databasePath,
|
||||||
|
TemplatePath: *templatePath,
|
||||||
|
StaticPath: *staticPath,
|
||||||
|
CloudflareToken: cloudflareToken,
|
||||||
|
CloudflareZone: cloudflareZone,
|
||||||
|
Port: *port,
|
||||||
|
Server: *server,
|
||||||
|
Migrate: *migrate,
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments, nil
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeConn(databasePath *string) *sql.DB {
|
||||||
|
log.Println("opening database at", *databasePath, "with foreign keys enabled")
|
||||||
|
dbConn, err := sql.Open("sqlite3", *databasePath+"?_foreign_keys=on")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbConn
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"database/sql"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Migrator func(*sql.DB) (*sql.DB, error)
|
||||||
|
|
||||||
|
func MigrateUsers(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
|
log.Println("migrating users table")
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);`)
|
||||||
|
if err != nil {
|
||||||
|
return dbConn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("creating unique index on users table")
|
||||||
|
_, err = dbConn.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users (username);`)
|
||||||
|
if err != nil {
|
||||||
|
return dbConn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MigrateApiKeys(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
|
log.Println("migrating api_keys table")
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS api_keys (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);`)
|
||||||
|
if err != nil {
|
||||||
|
return dbConn, err
|
||||||
|
}
|
||||||
|
return dbConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
|
log.Println("migrating dns_records table")
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS dns_records (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
ttl INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);`)
|
||||||
|
if err != nil {
|
||||||
|
return dbConn, err
|
||||||
|
}
|
||||||
|
return dbConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Migrate(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
|
log.Println("migrating database")
|
||||||
|
|
||||||
|
migrations := []Migrator{
|
||||||
|
MigrateUsers,
|
||||||
|
MigrateApiKeys,
|
||||||
|
MigrateDNSRecords,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, migration := range migrations {
|
||||||
|
dbConn, err := migration(dbConn)
|
||||||
|
if err != nil {
|
||||||
|
return dbConn, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbConn, nil
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
package database
|
||||||
|
|
||||||
func getUsers() {
|
func getUsers() {
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
restart: always
|
||||||
|
#image: ghcr.io/utahstate/slack-incidents
|
||||||
|
build: .
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:8080/api/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./db:/app/db
|
||||||
|
- ./templates:/app/templates
|
||||||
|
- ./static:/app/static
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:4455:8080"
|
7
go.mod
7
go.mod
|
@ -1,3 +1,8 @@
|
||||||
module hatecomputers.club/m
|
module git.hatecomputers.club/hatecomputers/hatecomputers.club
|
||||||
|
|
||||||
go 1.22.1
|
go 1.22.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
@ -0,0 +1,44 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("could not load .env file:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
argv, err := args.GetArgs()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn := database.MakeConn(&argv.DatabasePath)
|
||||||
|
defer dbConn.Close()
|
||||||
|
|
||||||
|
if argv.Migrate {
|
||||||
|
_, err = database.Migrate(dbConn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("database migrated successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
if argv.Server {
|
||||||
|
server := api.MakeServer(argv, dbConn)
|
||||||
|
log.Println("server listening on port", argv.Port)
|
||||||
|
err = server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/main.go
13
src/main.go
|
@ -1,13 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := godotenv.Load()
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
.blinky {
|
||||||
|
animation: blinker 1s step-start infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blinker {
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
:root {
|
||||||
|
--background-color: #f4e8e9;
|
||||||
|
--text-color: #333;
|
||||||
|
--link-color: #d291bc;
|
||||||
|
--container-bg: #fff7f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="DARK"] {
|
||||||
|
--background-color: #333;
|
||||||
|
--text-color: #f4e8e9;
|
||||||
|
--link-color: #b86b77;
|
||||||
|
--container-bg: #424242;
|
||||||
|
}
|
|
@ -1,25 +1,23 @@
|
||||||
:root {
|
@import "/static/css/colors.css";
|
||||||
/* Light theme colors */
|
@import "/static/css/blinky.css";
|
||||||
--background-color: #F4E8E9; /* Soft pink background */
|
|
||||||
--text-color: #333; /* Dark text for contrast */
|
@font-face {
|
||||||
--link-color: #D291BC; /* Retro pink for links */
|
font-family: "ComicSans";
|
||||||
--container-bg: #FFF7F8; /* Very light pink for containers */
|
src: url("/static/font/comicsans.ttf");
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode {
|
* {
|
||||||
/* Dark theme colors */
|
box-sizing: border-box;
|
||||||
--background-color: #333; /* Dark background */
|
margin: 0;
|
||||||
--text-color: #F4E8E9; /* Light text for contrast */
|
padding: 0;
|
||||||
--link-color: #B86B77; /* Soft pink for links */
|
color: var(--text-color);
|
||||||
--container-bg: #424242; /* Darker shade for containers */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'ComicSans', sans-serif;
|
font-family: "ComicSans", sans-serif;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
background-image: url("/static/img/stars.gif");
|
||||||
padding: 20px;
|
min-height: 100vh;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -33,10 +31,16 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 600px;
|
max-width: 1600px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
margin-top: 1rem;
|
||||||
background-color: var(--container-bg);
|
background-color: var(--container-bg);
|
||||||
padding: 20px;
|
padding: 1rem;
|
||||||
border-radius: 8px;
|
}
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--text-color);
|
||||||
|
|
||||||
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
|
@ -0,0 +1,27 @@
|
||||||
|
const THEMES = {
|
||||||
|
DARK: "DARK",
|
||||||
|
LIGHT: "LIGHT",
|
||||||
|
};
|
||||||
|
|
||||||
|
const flipFlopTheme = (theme) =>
|
||||||
|
THEMES[theme] === THEMES.DARK ? THEMES.LIGHT : THEMES.DARK;
|
||||||
|
|
||||||
|
const themePickerText = {
|
||||||
|
DARK: "light mode.",
|
||||||
|
LIGHT: "dark mode.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeSwitcher = document.getElementById("theme-switcher");
|
||||||
|
|
||||||
|
const setTheme = (theme) => {
|
||||||
|
themeSwitcher.textContent = `${themePickerText[theme]}`;
|
||||||
|
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
themeSwitcher.addEventListener("click", () =>
|
||||||
|
setTheme(flipFlopTheme(document.documentElement.getAttribute("data-theme"))),
|
||||||
|
);
|
||||||
|
|
||||||
|
setTheme(localStorage.getItem("theme") ?? THEMES.LIGHT);
|
File diff suppressed because one or more lines are too long
|
@ -1 +1,2 @@
|
||||||
console.log("hello world");
|
const scripts = ["/static/js/components/themeSwitcher.js"];
|
||||||
|
requirejs(scripts);
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
// sets theme before rendering & jquery loaded to prevent flashing of uninitialized theme
|
||||||
|
// (ugly white background)
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-theme",
|
||||||
|
localStorage.getItem("theme"),
|
||||||
|
);
|
|
@ -0,0 +1,7 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>page not found</h1>
|
||||||
|
<p><em>but hey, at least you found our witty 404 page. that's something, right?</em></p>
|
||||||
|
|
||||||
|
<p><a href="/">go back home</a></p>
|
||||||
|
|
||||||
|
{{ end }}
|
|
@ -6,14 +6,32 @@
|
||||||
<title>hatecomputers.club</title>
|
<title>hatecomputers.club</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||||
|
<link rel="icon" href="/static/img/favicon.svg">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/styles.css">
|
<link rel="stylesheet" type="text/css" href="/static/css/styles.css">
|
||||||
|
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://hatecomputers.club">
|
||||||
|
<meta property="og:title" content="hatecomputers.club">
|
||||||
|
<meta property="og:description" content="be decent at computer but hate. really bad for human.">
|
||||||
|
<meta property="og:image:secure_url" content="https://tunnel.simponic.xyz/static/img/favicon.svg">
|
||||||
|
<meta property="og:image:secure" content="https://tunnel.simponic.xyz/static/img/favicon.svg">
|
||||||
|
<script src="/static/js/util/setThemeBeforeRender.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>hatecomputers.club</h1>
|
||||||
|
<a href="javascript:void(0);" id="theme-switcher"></a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
{{ template "content" . }}
|
{{ template "content" . }}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="blinky">hi</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/script.js"></script>
|
<script data-main="/static/js/script.js" src="/static/js/require.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ define "base" }}
|
||||||
|
{{ template "content" . }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>guestbook</h1>
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue