chore: fresh restart

This commit is contained in:
Guz
2025-03-01 19:35:33 -03:00
parent e16e57f387
commit 4063a6fb0d
55 changed files with 44 additions and 14993 deletions

0
.env Normal file
View File

8
.gitignore vendored
View File

@@ -1,8 +0,0 @@
*.env
*_templ.go
*_templ.txt
node_modules/
bin/
tmp/
.dist/
assets/css/uno.css

20
.golangci.yml Normal file
View File

@@ -0,0 +1,20 @@
run:
timeout: 5m
modules-download-mode: readonly
linters:
disable-all: true
enable:
- errcheck
- goimports
- gofumpt
- revive # golint
- gosimple
- govet
- ineffassign
- staticcheck
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

16
app.go Normal file
View File

@@ -0,0 +1,16 @@
package capytalcodecomicverse
import (
"io/fs"
"net/http"
)
type App struct {
templates fs.FS
}
func NewApp(templates fs.FS) *App {
return &App{
templates: templates,
}
}

View File

@@ -1,117 +0,0 @@
package app
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"forge.capytal.company/capytalcode/project-comicverse/assets"
"forge.capytal.company/capytalcode/project-comicverse/configs"
"forge.capytal.company/capytalcode/project-comicverse/handlers/pages"
devPages "forge.capytal.company/capytalcode/project-comicverse/handlers/pages/dev"
"forge.capytal.company/capytalcode/project-comicverse/lib/middleware"
"forge.capytal.company/capytalcode/project-comicverse/lib/router"
)
type App struct {
dev bool
port int
logger *slog.Logger
server *http.Server
assets http.Handler
}
type AppOpts struct {
Dev *bool
Port *int
Assets http.Handler
}
func NewApp(opts ...AppOpts) *App {
if len(opts) == 0 {
opts[0] = AppOpts{}
}
if opts[0].Dev == nil {
d := false
opts[0].Dev = &d
}
if opts[0].Port == nil {
d := 8080
opts[0].Port = &d
}
app := &App{
dev: *opts[0].Dev,
port: *opts[0].Port,
assets: opts[0].Assets,
}
configs.DEVELOPMENT = app.dev
app.setLogger()
app.setServer()
return app
}
func (a *App) setLogger() {
a.logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
}
func (a *App) setServer() {
r := router.NewRouter()
r.Use(middleware.NewLoggerMiddleware(a.logger))
if configs.DEVELOPMENT {
a.logger.Info("RUNNING IN DEVELOPMENT MODE")
r.Use(middleware.DevMiddleware)
r.Handle("/_dev", devPages.Routes())
} else {
r.Use(middleware.CacheMiddleware)
}
if configs.DEVELOPMENT && a.assets != nil {
r.Handle("/assets/", a.assets)
} else {
r.Handle("/assets/", http.StripPrefix("/assets/", http.FileServerFS(assets.ASSETS)))
}
r.Handle("/", pages.Routes(a.logger))
srv := http.Server{
Addr: fmt.Sprintf(":%v", a.port),
Handler: r,
}
a.server = &srv
}
func (a *App) Run() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := a.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
a.logger.Error("Listen and serve returned error", slog.String("error", err.Error()))
}
}()
<-ctx.Done()
a.logger.Info("Gracefully shutting doing server")
if err := a.server.Shutdown(context.TODO()); err != nil {
a.logger.Error("Server shut down returned an error", slog.String("error", err.Error()))
}
a.logger.Info("FINAL")
}

View File

@@ -1,8 +0,0 @@
package assets
import (
"embed"
)
//go:embed css fonts lib
var ASSETS embed.FS

View File

@@ -1,175 +0,0 @@
@font-face {
font-family: "Karla";
font-style: normal;
src: url("/assets/fonts/KarlaVF.woff2") format("woff2"),
url("/assets/fonts/KarlaVF.ttf") format("truetype"),
url("/assets/fonts/KarlaMedium.otf") format("opentype");
}
@font-face {
font-family: "Karla";
font-style: italic;
src: url("/assets/fonts/KarlaItalicVF.woff2") format("woff2"),
url("/assets/fonts/KarlaItalicVF.ttf") format("truetype"),
url("/assets/fonts/KarlaItalicMedium.otf") format("opentype");
}
@font-face {
font-family: "Playfair";
font-style: normal;
src: url("/assets/fonts/PlayfairRomanVF.woff2") format("woff2"),
url("/assets/fonts/PlayfairRomanVF.ttf") format("truetype"),
url("/assets/fonts/PlayfairDisplay.otf") format("opentype");
}
@font-face {
font-family: "Playfair";
font-style: italic;
src: url("/assets/fonts/PlayfairItalicVF.woff2") format("woff2"),
url("/assets/fonts/PlayfairItalicVF.ttf") format("truetype"),
url("/assets/fonts/PlayfairDisplayItalic.otf") format("opentype");
}
:root * {
--theme-accent-hue: var(--user-theme-accent-hue, 280);
--theme-accent-saturation: var(--user-theme-accent-saturation, 95%);
--theme-accent-10: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 7%);
--theme-accent-20: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 10%);
--theme-accent-30: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 13%);
--theme-accent-40: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 16%);
--theme-accent-50: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 19%);
--theme-accent-60: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 23%);
--theme-accent-70: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 28%);
--theme-accent-80: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 38%);
--theme-accent-90: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 43%);
--theme-accent-100: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 48%);
--theme-accent-110: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 71%);
--theme-accent-120: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 93%);
--theme-neutral-hue: var(--user-theme-neutral-hue, 0);
--theme-neutral-saturation: var(--user-theme-neutral-saturation, 0%);
--theme-neutral-10: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 7%);
--theme-neutral-20: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 10%);
--theme-neutral-30: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 13%);
--theme-neutral-40: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 16%);
--theme-neutral-50: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 19%);
--theme-neutral-60: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 23%);
--theme-neutral-70: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 28%);
--theme-neutral-80: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 38%);
--theme-neutral-90: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 43%);
--theme-neutral-100: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 48%);
--theme-neutral-110: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 71%);
--theme-neutral-120: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 93%);
--theme-success-hue: var(--user-theme-sucesss-hue, 120);
--theme-success-saturation: var(--user-theme-sucesss-saturation, 95%);
--theme-success-10: hsl(var(--theme-success-hue), var(--theme-success-saturation), 7%);
--theme-success-20: hsl(var(--theme-success-hue), var(--theme-success-saturation), 10%);
--theme-success-30: hsl(var(--theme-success-hue), var(--theme-success-saturation), 13%);
--theme-success-40: hsl(var(--theme-success-hue), var(--theme-success-saturation), 16%);
--theme-success-50: hsl(var(--theme-success-hue), var(--theme-success-saturation), 19%);
--theme-success-60: hsl(var(--theme-success-hue), var(--theme-success-saturation), 23%);
--theme-success-70: hsl(var(--theme-success-hue), var(--theme-success-saturation), 28%);
--theme-success-80: hsl(var(--theme-success-hue), var(--theme-success-saturation), 38%);
--theme-success-90: hsl(var(--theme-success-hue), var(--theme-success-saturation), 43%);
--theme-success-100: hsl(var(--theme-success-hue), var(--theme-success-saturation), 48%);
--theme-success-110: hsl(var(--theme-success-hue), var(--theme-success-saturation), 71%);
--theme-success-120: hsl(var(--theme-success-hue), var(--theme-success-saturation), 93%);
--theme-danger-hue: var(--user-theme-danger-hue, 10);
--theme-danger-saturation: var(--user-theme-danger-saturation, 95%);
--theme-danger-10: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 7%);
--theme-danger-20: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 10%);
--theme-danger-30: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 13%);
--theme-danger-40: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 16%);
--theme-danger-50: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 19%);
--theme-danger-60: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 23%);
--theme-danger-70: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 28%);
--theme-danger-80: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 38%);
--theme-danger-90: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 43%);
--theme-danger-100: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 48%);
--theme-danger-110: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 71%);
--theme-danger-120: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 93%);
--theme-warn-hue: var(--user-theme-warn-hue, 60);
--theme-warn-saturation: var(--user-theme-warn-saturation, 95%);
--theme-warn-10: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 7%);
--theme-warn-20: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 10%);
--theme-warn-30: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 13%);
--theme-warn-40: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 16%);
--theme-warn-50: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 19%);
--theme-warn-60: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 23%);
--theme-warn-70: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 28%);
--theme-warn-80: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 38%);
--theme-warn-90: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 43%);
--theme-warn-100: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 48%);
--theme-warn-110: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 71%);
--theme-warn-120: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 93%);
}
@media (prefers-color-scheme: light) {
:root * {
--theme-accent-10: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 99%);
--theme-accent-20: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 98%);
--theme-accent-30: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 94%);
--theme-accent-40: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 91%);
--theme-accent-50: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 88%);
--theme-accent-60: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 85%);
--theme-accent-70: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 81%);
--theme-accent-80: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 73%);
--theme-accent-90: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 55%);
--theme-accent-100: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 51%);
--theme-accent-110: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 39%);
--theme-accent-120: hsl(var(--theme-accent-hue), var(--theme-accent-saturation), 13%);
--theme-neutral-10: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 99%);
--theme-neutral-20: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 98%);
--theme-neutral-30: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 94%);
--theme-neutral-40: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 91%);
--theme-neutral-50: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 88%);
--theme-neutral-60: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 85%);
--theme-neutral-70: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 81%);
--theme-neutral-80: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 73%);
--theme-neutral-90: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 55%);
--theme-neutral-100: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 51%);
--theme-neutral-110: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 39%);
--theme-neutral-120: hsl(var(--theme-neutral-hue), var(--theme-neutral-saturation), 13%);
--theme-success-10: hsl(var(--theme-success-hue), var(--theme-success-saturation), 99%);
--theme-success-20: hsl(var(--theme-success-hue), var(--theme-success-saturation), 98%);
--theme-success-30: hsl(var(--theme-success-hue), var(--theme-success-saturation), 94%);
--theme-success-40: hsl(var(--theme-success-hue), var(--theme-success-saturation), 91%);
--theme-success-50: hsl(var(--theme-success-hue), var(--theme-success-saturation), 88%);
--theme-success-60: hsl(var(--theme-success-hue), var(--theme-success-saturation), 85%);
--theme-success-70: hsl(var(--theme-success-hue), var(--theme-success-saturation), 81%);
--theme-success-80: hsl(var(--theme-success-hue), var(--theme-success-saturation), 73%);
--theme-success-90: hsl(var(--theme-success-hue), var(--theme-success-saturation), 55%);
--theme-success-100: hsl(var(--theme-success-hue), var(--theme-success-saturation), 51%);
--theme-success-110: hsl(var(--theme-success-hue), var(--theme-success-saturation), 39%);
--theme-success-120: hsl(var(--theme-success-hue), var(--theme-success-saturation), 13%);
--theme-danger-10: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 99%);
--theme-danger-20: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 98%);
--theme-danger-30: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 94%);
--theme-danger-40: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 91%);
--theme-danger-50: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 88%);
--theme-danger-60: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 85%);
--theme-danger-70: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 81%);
--theme-danger-80: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 73%);
--theme-danger-90: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 55%);
--theme-danger-100: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 51%);
--theme-danger-110: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 39%);
--theme-danger-120: hsl(var(--theme-danger-hue), var(--theme-danger-saturation), 13%);
--theme-warn-10: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 99%);
--theme-warn-20: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 98%);
--theme-warn-30: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 94%);
--theme-warn-40: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 91%);
--theme-warn-50: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 88%);
--theme-warn-60: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 85%);
--theme-warn-70: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 81%);
--theme-warn-80: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 73%);
--theme-warn-90: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 55%);
--theme-warn-100: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 51%);
--theme-warn-110: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 39%);
--theme-warn-120: hsl(var(--theme-warn-hue), var(--theme-warn-saturation), 13%);
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,93 +0,0 @@
Copyright 2019 The Karla Project Authors (https://github.com/googlefonts/karla)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,93 +0,0 @@
Copyright 20052023 The Playfair Project Authors (https://github.com/clauseggers/Playfair)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,18 +0,0 @@
# Fonts used
## Karla
This project uses the [Karla font](https://github.com/googlefonts/karla/tree/main),
created by [Jonathan Pinhorn](http://twitter.com/jonpinhorn_type) and released through
Google Webfonts in 2012. Karla font is released under the SIL Open Font License, Version 1.1,
which a copy can be found [locally](./LICENSE-Karla), [on GitHub](https://github.com/googlefonts/karla/blob/main/OFL.txt)
and [on the OFL's site](https://scripts.sil.org/OFL).
## Playfair Roman, Playfair Display & Playfair Italic
This project uses the [Playfair font family, version 2.1](https://github.com/clauseggers/Playfair),
created by [Claus Eggers Sørensen](http://forthehearts.net/) and released under the
SIL Open Font License Version 1.1, which a copy can be found [locally](./LICENSE-Playfair),
[on GitHub](https://github.com/clauseggers/Playfair/blob/master/OFL.txt) and
[on the OFL's site](https://scripts.sil.org/OFL)

View File

@@ -1,3 +0,0 @@
import * as hello from './hello.js';
hello.hello();

View File

@@ -1,6 +0,0 @@
/**
* Says hello!
*/
export function hello() {
console.log('hello world');
}

239
assets/lib/htmx.d.ts vendored
View File

@@ -1,239 +0,0 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/**
* @copyright Big Sky Software 2024
* @license 0BSD
* @copyright Big Sky Software 2024
* @author Big Sky Software <https://github.com/bigskysoftware>
*
* This source code is copied from HTMX's GitHub repository, located at
* https://github.com/bigskysoftware/htmx/blob/master/dist/htmx.esm.js.
*
* This source code and the original are licensed under the Zero-Clause BSD license,
* which a copy is available in the original [GitHub](https://github.com/bigskysoftware/htmx/blob/master/LICENSE)
* and here below:
*
* Zero-Clause BSD
* =============
*
* Permission to use, copy, modify, and/or distribute this software for
* any purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
* FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
* DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
* OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/* eslint-disable */
export default htmx;
export type HttpVerb = "get" | "head" | "post" | "put" | "delete" | "connect" | "options" | "trace" | "patch";
export type SwapOptions = {
select?: string;
selectOOB?: string;
eventInfo?: any;
anchor?: string;
contextElement?: Element;
afterSwapCallback?: swapCallback;
afterSettleCallback?: swapCallback;
};
export type swapCallback = () => any;
export type HtmxSwapStyle = "innerHTML" | "outerHTML" | "beforebegin" | "afterbegin" | "beforeend" | "afterend" | "delete" | "none" | string;
export type HtmxSwapSpecification = {
swapStyle: HtmxSwapStyle;
swapDelay: number;
settleDelay: number;
transition?: boolean;
ignoreTitle?: boolean;
head?: string;
scroll?: "top" | "bottom";
scrollTarget?: string;
show?: string;
showTarget?: string;
focusScroll?: boolean;
};
export type ConditionalFunction = ((this: Node, evt: Event) => boolean) & {
source: string;
};
export type HtmxTriggerSpecification = {
trigger: string;
pollInterval?: number;
eventFilter?: ConditionalFunction;
changed?: boolean;
once?: boolean;
consume?: boolean;
delay?: number;
from?: string;
target?: string;
throttle?: number;
queue?: string;
root?: string;
threshold?: string;
};
export type HtmxElementValidationError = {
elt: Element;
message: string;
validity: ValidityState;
};
export type HtmxHeaderSpecification = Record<string, string>;
export type HtmxAjaxHelperContext = {
source?: Element | string;
event?: Event;
handler?: HtmxAjaxHandler;
target?: Element | string;
swap?: HtmxSwapStyle;
values?: any | FormData;
headers?: Record<string, string>;
select?: string;
};
export type HtmxRequestConfig = {
boosted: boolean;
useUrlParams: boolean;
formData: FormData;
/**
* formData proxy
*/
parameters: any;
unfilteredFormData: FormData;
/**
* unfilteredFormData proxy
*/
unfilteredParameters: any;
headers: HtmxHeaderSpecification;
target: Element;
verb: HttpVerb;
errors: HtmxElementValidationError[];
withCredentials: boolean;
timeout: number;
path: string;
triggeringEvent: Event;
};
export type HtmxResponseInfo = {
xhr: XMLHttpRequest;
target: Element;
requestConfig: HtmxRequestConfig;
etc: HtmxAjaxEtc;
boosted: boolean;
select: string;
pathInfo: {
requestPath: string;
finalRequestPath: string;
responsePath: string | null;
anchor: string;
};
failed?: boolean;
successful?: boolean;
keepIndicators?: boolean;
};
export type HtmxAjaxEtc = {
returnPromise?: boolean;
handler?: HtmxAjaxHandler;
select?: string;
targetOverride?: Element;
swapOverride?: HtmxSwapStyle;
headers?: Record<string, string>;
values?: any | FormData;
credentials?: boolean;
timeout?: number;
};
export type HtmxResponseHandlingConfig = {
code?: string;
swap: boolean;
error?: boolean;
ignoreTitle?: boolean;
select?: string;
target?: string;
swapOverride?: string;
event?: string;
};
export type HtmxBeforeSwapDetails = HtmxResponseInfo & {
shouldSwap: boolean;
serverResponse: any;
isError: boolean;
ignoreTitle: boolean;
selectOverride: string;
swapOverride: string;
};
export type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any;
export type HtmxSettleTask = (() => void);
export type HtmxSettleInfo = {
tasks: HtmxSettleTask[];
elts: Element[];
title?: string;
};
export type HtmxExtension = {
init: (api: any) => void;
onEvent: (name: string, event: Event | CustomEvent) => boolean;
transformResponse: (text: string, xhr: XMLHttpRequest, elt: Element) => string;
isInlineSwap: (swapStyle: HtmxSwapStyle) => boolean;
handleSwap: (swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean | Node[];
encodeParameters: (xhr: XMLHttpRequest, parameters: FormData, elt: Node) => any | string | null;
getSelectors: () => string[] | null;
};
declare namespace htmx {
let onLoad: (callback: (elt: Node) => void) => EventListener;
let process: (elt: Element | string) => void;
let on: (arg1: EventTarget | string, arg2: string | EventListener, arg3?: EventListener | any | boolean, arg4?: any | boolean) => EventListener;
let off: (arg1: EventTarget | string, arg2: string | EventListener, arg3?: EventListener) => EventListener;
let trigger: (elt: EventTarget | string, eventName: string, detail?: any | undefined) => boolean;
let ajax: (verb: HttpVerb, path: string, context: Element | string | HtmxAjaxHelperContext) => Promise<void>;
let find: (eltOrSelector: ParentNode | string, selector?: string) => Element | null;
let findAll: (eltOrSelector: ParentNode | string, selector?: string) => NodeListOf<Element>;
let closest: (elt: Element | string, selector: string) => Element | null;
function values(elt: Element, type: HttpVerb): any;
let remove: (elt: Node, delay?: number) => void;
let addClass: (elt: Element | string, clazz: string, delay?: number) => void;
let removeClass: (node: Node | string, clazz: string, delay?: number) => void;
let toggleClass: (elt: Element | string, clazz: string) => void;
let takeClass: (elt: Node | string, clazz: string) => void;
let swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void;
let defineExtension: (name: string, extension: HtmxExtension) => void;
let removeExtension: (name: string) => void;
let logAll: () => void;
let logNone: () => void;
let logger: any;
namespace config {
let historyEnabled: boolean;
let historyCacheSize: number;
let refreshOnHistoryMiss: boolean;
let defaultSwapStyle: HtmxSwapStyle;
let defaultSwapDelay: number;
let defaultSettleDelay: number;
let includeIndicatorStyles: boolean;
let indicatorClass: string;
let requestClass: string;
let addedClass: string;
let settlingClass: string;
let swappingClass: string;
let allowEval: boolean;
let allowScriptTags: boolean;
let inlineScriptNonce: string;
let inlineStyleNonce: string;
let attributesToSettle: string[];
let withCredentials: boolean;
let timeout: number;
let wsReconnectDelay: "full-jitter" | ((retryCount: number) => number);
let wsBinaryType: BinaryType;
let disableSelector: string;
let scrollBehavior: "auto" | "instant" | "smooth";
let defaultFocusScroll: boolean;
let getCacheBusterParam: boolean;
let globalViewTransitions: boolean;
let methodsThatUseUrlParams: (HttpVerb)[];
let selfRequestsOnly: boolean;
let ignoreTitle: boolean;
let scrollIntoViewOnBoost: boolean;
let triggerSpecsCache: any | null;
let disableInheritance: boolean;
let responseHandling: HtmxResponseHandlingConfig[];
let allowNestedOobSwaps: boolean;
}
let parseInterval: (str: string) => number | undefined;
let _: (str: string) => any;
let version: string;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
package configs
const (
APP_NAME = "Comicverse"
APP_VERSION = "0.0.0"
)
var DEVELOPMENT = false

View File

@@ -1,240 +0,0 @@
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
import jsdoc from 'eslint-plugin-jsdoc';
// @ts-expect-error eslint-plugin-jsdoc does not have type definitions
import json from 'eslint-plugin-json';
// @ts-expect-error eslint-plugin-import does not have type definitions
import imports from 'eslint-plugin-import';
import perfectionist from 'eslint-plugin-perfectionist';
import unicorn from 'eslint-plugin-unicorn';
import wc from 'eslint-plugin-wc';
import globals from 'globals';
// eslint-disable-next-line import/no-unresolved
import ts from 'typescript-eslint';
/**
* @typedef {Readonly<import('eslint').Linter.Config>} Config
*/
/** @type {Config[]} */
const config = [
// Ignores
{
ignores: [
'node_modules',
'package-lock.json',
'package.json',
'tsconfig.json',
'.vscode',
'dist',
],
},
// Logic plugins
js.configs.recommended,
(/** @type {Config} */ (ts.configs.eslintRecommended)),
...(/** @type {Config[]} */ (ts.configs.strictTypeChecked)),
{
languageOptions: {
parserOptions: {
projectService: true,
// @ts-expect-error import.meta.dirname is not defined but works in NodeJS
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
tsconfigRootDir: import.meta.dirname,
},
},
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(/** @type {Config} */ (imports.flatConfigs.recommended)),
{
plugins: { wc: wc },
rules: { ...wc.configs.recommended.rules },
},
{
plugins: { unicorn: unicorn },
},
jsdoc.configs['flat/recommended-typescript-flavor'],
// Stylistic plugins
(/** @type {Config} */ (stylistic.configs.customize({
arrowParens: false,
braceStyle: 'stroustrup',
commaDangle: 'always-multiline',
indent: 'tab',
jsx: false,
quoteProps: 'consistent',
quotes: 'single',
semi: true,
}))),
(/** @type {Config} */ (perfectionist.configs['recommended-natural'])),
// Custom config
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2022,
globals: {
...globals.builtin,
...globals.es2022,
...globals.browser,
},
parserOptions: {
ecmaFeatures: {
impliedStrict: true,
},
},
sourceType: 'module',
},
rules: {
// Imports
'import/exports-last': 'error',
'import/extensions': ['error', 'always', { ignorePackages: true }],
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-absolute-path': 'error',
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-cycle': 'error',
'import/no-default-export': 'error',
'import/no-deprecated': 'error',
'import/no-dynamic-require': 'error',
'import/no-import-module-exports': 'error',
'import/no-nodejs-modules': 'error',
'import/no-relative-packages': 'error',
'import/no-relative-parent-imports': 'error',
'import/no-restricted-paths': 'error',
'import/no-unassigned-import': 'error',
'import/no-unused-modules': 'error',
'import/no-useless-path-segments': 'error',
'import/no-webpack-loader-syntax': 'error',
'import/order': 'off',
// JSDoc
'jsdoc/check-indentation': 'warn',
'jsdoc/check-line-alignment': 'warn',
'jsdoc/check-syntax': 'warn',
'jsdoc/check-template-names': 'warn',
'jsdoc/convert-to-jsdoc-comments': 'warn',
'jsdoc/informative-docs': 'warn',
'jsdoc/no-bad-blocks': 'warn',
'jsdoc/no-blank-block-descriptions': 'warn',
'jsdoc/no-blank-blocks': 'warn',
'jsdoc/require-asterisk-prefix': 'warn',
'jsdoc/require-description': 'warn',
'jsdoc/require-description-complete-sentence': 'warn',
'jsdoc/require-hyphen-before-param-description': 'warn',
'jsdoc/require-template': 'warn',
'jsdoc/require-throws': 'warn',
'jsdoc/sort-tags': 'warn',
// Globals
'no-restricted-globals': ['error'].concat(CONFUSING_BROWSER_GLOBALS()),
// Unicorn
'unicorn/better-regex': 'error',
'unicorn/custom-error-definition': 'error',
'unicorn/no-keyword-prefix': 'error',
'unicorn/no-unused-properties': 'error',
'unicorn/prefer-json-parse-buffer': 'error',
'unicorn/require-post-message-target-origin': 'error',
},
},
// Overrides
{
files: ['*.config.js'],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'import/no-default-export': 'off',
},
},
// Additional file types
{
files: ['**/*.json'],
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...json.configs['recommended'],
},
];
/**
* This list was copied from Facebook's create-react-app's "confusing-browser-globals" package,
* which is licensed under the MIT license.
*
* The original source code of this list is available on GitHub:
* https://github.com/facebook/create-react-app/blob/dd420a6d25d037decd7b81175626dfca817437ff/packages/confusing-browser-globals/index.js.
*
* The original LICENSE file can be found here:
* https://github.com/facebook/create-react-app/blob/dd420a6d25d037decd7b81175626dfca817437ff/packages/confusing-browser-globals/LICENSE.
* @copyright 2015-present, Facebook, Inc
* @license MIT
* @returns {string[]} - The globals.
* @author Facebook
*/
function CONFUSING_BROWSER_GLOBALS() {
return [
'addEventListener',
'blur',
'close',
'closed',
'confirm',
'defaultStatus',
'defaultstatus',
'event',
'external',
'find',
'focus',
'frameElement',
'frames',
'history',
'innerHeight',
'innerWidth',
'length',
'location',
'locationbar',
'menubar',
'moveBy',
'moveTo',
'name',
'onblur',
'onerror',
'onfocus',
'onload',
'onresize',
'onunload',
'open',
'opener',
'opera',
'outerHeight',
'outerWidth',
'pageXOffset',
'pageYOffset',
'parent',
'print',
'removeEventListener',
'resizeBy',
'resizeTo',
'screen',
'screenLeft',
'screenTop',
'screenX',
'screenY',
'scroll',
'scrollbars',
'scrollBy',
'scrollTo',
'scrollX',
'scrollY',
'self',
'status',
'statusbar',
'stop',
'toolbar',
'top',
];
}
export default config;

154
flake.lock generated
View File

@@ -1,81 +1,5 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1722589758,
"narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=",
"owner": "nix-community",
"repo": "gomod2nix",
"rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1726243404,
@@ -92,85 +16,9 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1724322575,
"narHash": "sha256-kRYwAdYsaICNb2WYcWtBFG6caSuT0v/vTAyR8ap0IR0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2a02822b466ffb9f1c02d07c5dd6b96d08b56c6b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"templ": "templ"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"templ": {
"inputs": {
"gitignore": "gitignore",
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs_2",
"xc": "xc"
},
"locked": {
"lastModified": 1725786353,
"narHash": "sha256-lU8aVTw73HX0lNGPyD8Xnvtnr2VFTXv/S6xCVn6Lg74=",
"owner": "a-h",
"repo": "templ",
"rev": "e2511cd57e5ecd28ce6e3d944c87f1e31e20b596",
"type": "github"
},
"original": {
"owner": "a-h",
"ref": "v0.2.778",
"repo": "templ",
"type": "github"
}
},
"xc": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1724404748,
"narHash": "sha256-p6rXzNiDm2uBvO1MLzC5pJp/0zRNzj/snBzZI0ce62s=",
"owner": "joerdav",
"repo": "xc",
"rev": "960ff9f109d47a19122cfb015721a76e3a0f23a2",
"type": "github"
},
"original": {
"owner": "joerdav",
"repo": "xc",
"type": "github"
"nixpkgs": "nixpkgs"
}
}
},

View File

@@ -2,13 +2,8 @@
description = "My development environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
templ.url = "github:a-h/templ?ref=v0.2.778";
};
outputs = {
self,
nixpkgs,
...
} @ inputs: let
outputs = {nixpkgs, ...}: let
systems = [
"x86_64-linux"
"aarch64-linux"
@@ -20,7 +15,6 @@
pkgs = import nixpkgs {inherit system;};
in
f system pkgs);
templ = system: inputs.templ.packages.${system}.templ;
in {
devShells = forAllSystems (system: pkgs: {
default = pkgs.mkShell {
@@ -28,19 +22,12 @@
hardeningDisable = ["all"];
buildInputs = with pkgs; [
# Javascript tools
eslint_d
nodejs_22
nodePackages_latest.eslint
# Go tools
go
gofumpt
golangci-lint
golines
gofumpt
gotools
delve
(templ system)
# Sqlite tools
sqlite

2
go.mod
View File

@@ -1,5 +1,3 @@
module forge.capytal.company/capytalcode/project-comicverse
go 1.22.7
require github.com/a-h/templ v0.2.778 // indirect

2
go.sum
View File

@@ -1,2 +0,0 @@
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=

View File

@@ -1,64 +0,0 @@
package pages
import (
"net/http"
"log"
"strconv"
"forge.capytal.company/capytalcode/project-comicverse/lib/router/rerrors"
"forge.capytal.company/capytalcode/project-comicverse/lib/cookies"
"forge.capytal.company/capytalcode/project-comicverse/lib/forms"
"forge.capytal.company/capytalcode/project-comicverse/templates/layouts"
)
type Dashboard struct {
Message string `form:"message"`
Limit int `form:"limit"`
Optional *string `form:"optional"`
}
type DashboardCookie struct {
Hello string `cookie:"dashboard-cookie"`
Bool bool
Test int
}
func (p *Dashboard) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := forms.Unmarshal(r, p); err != nil {
forms.RerrUnsmarshal(err).ServeHTTP(w, r)
return
}
hasCookie := true
c := DashboardCookie{"hello world", true, 0}
if _, err := cookies.UnmarshalIfRequest(r, &c); err != nil {
rerrors.InternalError(err).ServeHTTP(w, r)
return
}
log.Print(hasCookie, c)
if err := cookies.MarshalToWriter(c, w); err != nil {
rerrors.InternalError(err).ServeHTTP(w, r)
}
if err := p.Component().Render(r.Context(), w); err != nil {
rerrors.InternalError(err).ServeHTTP(w, r)
return
}
}
templ (p *Dashboard) Component() {
@layouts.Page() {
<div class="text-danger-100 font-sans">
<p>{ p.Message }</p>
<p>{ strconv.Itoa(p.Limit) }</p>
if p.Optional != nil {
<p>{ *p.Optional }</p>
} else {
<p>nil</p>
}
</div>
}
}

View File

@@ -1,95 +0,0 @@
package dev
import (
"net/http"
"fmt"
"strings"
"forge.capytal.company/capytalcode/project-comicverse/lib/router/rerrors"
"forge.capytal.company/capytalcode/project-comicverse/templates/layouts"
)
type Colors struct{}
func (p *Colors) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := p.Component().Render(r.Context(), w); err != nil {
rerrors.InternalError(err).ServeHTTP(w, r)
return
}
}
templ (p *Colors) Heading() {
<script type="module" src="/assets/javascript/pages/devcolors.js" defer></script>
}
templ (p *Colors) Component() {
@layouts.Page(layouts.PageInfo{
Heading: p.Heading(),
}) {
<div class="m-10 flex flex-col gap-5 font-sans">
<article>
<input
id="accent-color-hue"
class="w-full"
type="range"
value="260"
min="0"
max="360"
/>
<details>
<summary class="font-bold">Pallete</summary>
<ul class="list-none p-0 flex flex-col gap-3">
@templ.Raw(p.html())
</ul>
</details>
</article>
<article class="grid grid-cols-3">
<section class="bg-neutral-20 p-5">
<button>Hello world</button>
</section>
</article>
</div>
}
}
func (p *Colors) html() string {
cs := []string{}
for _, c := range colors {
ss := []string{"<li class=\"w-full\">" +
"<p class=\"mb-0\">" + c + "</p>" +
"<ul class=\"flex list-none p-0 w-full\">"}
for _, s := range scales {
ss = append(ss, fmt.Sprintf("<li "+
"style=\"background-color:var(--theme-%s-%s); width: 10%%; height: 3rem;\""+
"></li>", c, s))
}
ss = append(ss, "</ul></li>")
cs = append(cs, strings.Join(ss, ""))
}
return strings.Join(cs, "")
}
var colors = []string{
"accent",
"neutral",
"danger",
"success",
"warn",
}
var scales = []string{
"10",
"20",
"30",
"40",
"50",
"60",
"70",
"80",
"90",
"100",
"110",
"120",
}

View File

@@ -1,18 +0,0 @@
package dev
import (
"net/http"
"forge.capytal.company/capytalcode/project-comicverse/lib/router"
)
func Routes() router.Router {
r := router.NewRouter()
r.Handle("/colors", &Colors{})
r.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = w.Write([]byte("hello world"))
})
return r
}

View File

@@ -1,25 +0,0 @@
package pages
import (
"fmt"
"forge.capytal.company/capytalcode/project-comicverse/lib/router/rerrors"
"forge.capytal.company/capytalcode/project-comicverse/templates/layouts"
)
type ErrorPage struct{}
templ (p ErrorPage) Component(err rerrors.RouteError) {
@layouts.Page() {
<main>
<h1>Error</h1>
<p>{ fmt.Sprintf("%#v", err) }</p>
for k, v := range err.Info {
<p>{ k } { fmt.Sprint(v) } </p>
}
if err.Endpoint != "" {
<a href={ templ.SafeURL(err.Endpoint) }>Retry</a>
}
</main>
}
}

View File

@@ -1,25 +0,0 @@
package pages
import (
"log/slog"
"net/http"
"forge.capytal.company/capytalcode/project-comicverse/lib/router"
"forge.capytal.company/capytalcode/project-comicverse/lib/router/rerrors"
)
func Routes(logger *slog.Logger) router.Router {
r := router.NewRouter()
r.Use(rerrors.NewErrorMiddleware(ErrorPage{}.Component, logger))
r.Handle("/dashboard", &Dashboard{})
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
rerrors.NotFound().ServeHTTP(w, r)
}
})
return r
}

View File

@@ -1,298 +0,0 @@
package cookies
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"forge.capytal.company/capytalcode/project-comicverse/lib/router/rerrors"
)
type Marshaler interface {
MarshalCookie() (*http.Cookie, error)
}
type Unmarshaler interface {
UnmarshalCookie(*http.Cookie) error
}
func Marshal(v any) (*http.Cookie, error) {
if m, ok := v.(Marshaler); ok {
return m.MarshalCookie()
}
c, err := marshalValue(v)
if err != nil {
return c, err
}
if err := setCookieProps(c, v); err != nil {
return c, err
}
return c, err
}
func MarshalToWriter(v any, w http.ResponseWriter) error {
if ck, err := Marshal(v); err != nil {
return err
} else {
http.SetCookie(w, ck)
}
return nil
}
func Unmarshal(c *http.Cookie, v any) error {
if m, ok := v.(Unmarshaler); ok {
return m.UnmarshalCookie(c)
}
value := c.Value
b, err := base64.URLEncoding.DecodeString(value)
if err != nil {
return errors.Join(ErrDecodeBase64, err)
}
if err := json.Unmarshal(b, v); err != nil {
return errors.Join(ErrUnmarshal, err)
}
return nil
}
func UnmarshalRequest(r *http.Request, v any) error {
name, err := getCookieName(v)
if err != nil {
return err
}
c, err := r.Cookie(name)
if errors.Is(err, http.ErrNoCookie) {
return ErrNoCookie{name}
} else if err != nil {
return err
}
return Unmarshal(c, v)
}
func UnmarshalIfRequest(r *http.Request, v any) (bool, error) {
if err := UnmarshalRequest(r, v); err != nil {
if _, ok := err.(ErrNoCookie); ok {
return false, nil
} else {
return true, err
}
} else {
return true, nil
}
}
func RerrUnmarshalCookie(err error) rerrors.RouteError {
if e, ok := err.(ErrNoCookie); ok {
return rerrors.MissingCookies([]string{e.name})
} else {
return rerrors.InternalError(err)
}
}
func marshalValue(v any) (*http.Cookie, error) {
b, err := json.Marshal(v)
if err != nil {
return &http.Cookie{}, errors.Join(ErrMarshal, err)
}
s := base64.URLEncoding.EncodeToString(b)
return &http.Cookie{
Value: s,
}, nil
}
var COOKIE_EXPIRE_VALID_FORMATS = []string{
time.DateOnly, time.DateTime,
time.RFC1123, time.RFC1123Z,
}
func setCookieProps(c *http.Cookie, v any) error {
tag, err := getCookieTag(v)
if err != nil {
return err
}
c.Name, err = getCookieName(v)
if err != nil {
return err
}
tvs := strings.Split(tag, ",")
if len(tvs) == 1 {
return nil
}
tvs = tvs[1:]
for _, tv := range tvs {
var k, v string
if strings.Contains(tv, "=") {
s := strings.Split(tv, "=")
k = s[0]
v = s[1]
} else {
k = tv
v = ""
}
switch k {
case "SECURE":
c.Name = "__Secure-" + c.Name
c.Secure = true
case "HOST":
c.Name = "__Host" + c.Name
c.Secure = true
c.Path = "/"
case "path":
c.Path = v
case "domain":
c.Domain = v
case "httponly":
if v == "" {
c.HttpOnly = true
} else if v, err := strconv.ParseBool(v); err != nil {
c.HttpOnly = false
} else {
c.HttpOnly = v
}
case "samesite":
if v == "" {
c.SameSite = http.SameSiteDefaultMode
} else if v == "strict" {
c.SameSite = http.SameSiteStrictMode
} else if v == "lax" {
c.SameSite = http.SameSiteLaxMode
} else {
c.SameSite = http.SameSiteNoneMode
}
case "secure":
if v == "" {
c.Secure = true
} else if v, err := strconv.ParseBool(v); err != nil {
c.Secure = false
} else {
c.Secure = v
}
case "max-age", "age":
if v == "" {
c.MaxAge = 0
} else if v, err := strconv.Atoi(v); err != nil {
c.MaxAge = 0
} else {
c.MaxAge = v
}
case "expires":
if v == "" {
c.Expires = time.Now()
} else if v, err := timeParseMultiple(v, COOKIE_EXPIRE_VALID_FORMATS...); err != nil {
c.Expires = time.Now()
} else {
c.Expires = v
}
}
}
return nil
}
func getCookieName(v any) (name string, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Join(ErrReflectPanic, fmt.Errorf("Panic recovered: %#v", r))
}
}()
tag, err := getCookieTag(v)
if err != nil {
return name, err
}
tvs := strings.Split(tag, ",")
if len(tvs) == 0 {
t := reflect.TypeOf(v)
name = t.Name()
} else {
name = tvs[0]
}
if name == "" {
return name, ErrMissingName
}
return name, nil
}
func getCookieTag(v any) (t string, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Join(ErrReflectPanic, fmt.Errorf("Panic recovered: %#v", r))
}
}()
rt := reflect.TypeOf(v)
if rt.Kind() == reflect.Pointer {
rt = rt.Elem()
}
for i := 0; i < rt.NumField(); i++ {
ft := rt.Field(i)
if t := ft.Tag.Get("cookie"); t != "" {
return t, nil
}
}
return "", nil
}
func timeParseMultiple(v string, formats ...string) (time.Time, error) {
errs := []error{}
for _, f := range formats {
t, err := time.Parse(v, f)
if err != nil {
errs = append(errs, err)
} else {
return t, nil
}
}
return time.Time{}, errs[len(errs)-1]
}
var (
ErrDecodeBase64 = errors.New("Failed to decode base64 string from cookie value")
ErrMarshal = errors.New("Failed to marhal JSON value for cookie value")
ErrUnmarshal = errors.New("Failed to unmarshal JSON value from cookie value")
ErrReflectPanic = errors.New("Reflect panic while trying to get tag from value")
ErrMissingName = errors.New("Failed to get name of cookie")
)
type ErrNoCookie struct {
name string
}
func (e ErrNoCookie) Error() string {
return fmt.Sprintf("Cookie \"%s\" missing from request", e.name)
}

View File

@@ -1,215 +0,0 @@
package forms
import (
"errors"
"fmt"
"log"
"net/http"
"reflect"
"strconv"
"strings"
"forge.capytal.company/capytalcode/project-comicverse/lib/router/rerrors"
)
type Unmarshaler interface {
UnmarshalForm(r *http.Request) error
}
func Unmarshal(r *http.Request, v any) (err error) {
if u, ok := v.(Unmarshaler); ok {
return u.UnmarshalForm(r)
}
defer func() {
if r := recover(); r != nil {
err = errors.Join(ErrReflectPanic, fmt.Errorf("Panic recovered: %#v", r))
}
}()
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
rt := rv.Type()
for i := 0; i < rv.NumField(); i++ {
ft := rt.Field(i)
fv := rv.FieldByName(ft.Name)
log.Print(ft.Name)
if !fv.CanSet() {
continue
}
// TODO: Support embedded fields
if ft.Anonymous {
continue
}
var tv string
if t := ft.Tag.Get("form"); t != "" {
tv = t
} else if t = ft.Tag.Get("query"); t != "" {
tv = t
} else {
tv = ft.Name
}
tvs := strings.Split(tv, ",")
name := tvs[0]
required := false
defaultv := ""
for _, v := range tvs {
if v == "required" {
required = true
} else if strings.HasPrefix(v, "default=") {
defaultv = strings.TrimPrefix(v, "default=")
}
}
qv := r.FormValue(name)
if qv == "" {
if defaultv != "" {
qv = defaultv
} else if required {
return &ErrMissingRequiredValue{name}
} else {
continue
}
}
if err := setFieldValue(fv, qv); errors.Is(err, &ErrInvalidValueType{}) {
e, _ := err.(*ErrInvalidValueType)
e.value = name
return e
} else if errors.Is(err, &ErrUnsuportedValueType{}) {
e, _ := err.(*ErrUnsuportedValueType)
e.value = name
return e
} else if err != nil {
return err
}
}
return nil
}
func RerrUnsmarshal(err error) rerrors.RouteError {
if e, ok := err.(*ErrMissingRequiredValue); ok {
return rerrors.MissingParameters([]string{e.value})
} else if e, ok := err.(*ErrInvalidValueType); ok {
return rerrors.BadRequest(e.Error())
} else {
return rerrors.InternalError(err)
}
}
func setFieldValue(rv reflect.Value, v string) error {
switch rv.Kind() {
case reflect.Pointer:
return setFieldValue(rv.Elem(), v)
case reflect.String:
rv.SetString(v)
case reflect.Bool:
if cv, err := strconv.ParseBool(v); err != nil {
return &ErrInvalidValueType{"bool", err, ""}
} else {
rv.SetBool(cv)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if cv, err := strconv.Atoi(v); err != nil {
return &ErrInvalidValueType{"int", err, ""}
} else {
rv.SetInt(int64(cv))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if cv, err := strconv.Atoi(v); err != nil {
return &ErrInvalidValueType{"uint", err, ""}
} else {
rv.SetUint(uint64(cv))
}
case reflect.Float32, reflect.Float64:
if cv, err := strconv.ParseFloat(v, 64); err != nil {
return &ErrInvalidValueType{"float64", err, ""}
} else {
rv.SetFloat(cv)
}
case reflect.Complex64, reflect.Complex128:
if cv, err := strconv.ParseComplex(v, 128); err != nil {
return &ErrInvalidValueType{"complex128", err, ""}
} else {
rv.SetComplex(cv)
}
// TODO: Support strucys
// TODO: Support slices
// TODO: Support maps
default:
return &ErrUnsuportedValueType{
[]string{
"string",
"bool",
"int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64",
"complex64", "complex64",
},
"",
}
}
return nil
}
type ErrInvalidValueType struct {
expected string
err error
value string
}
func (e ErrInvalidValueType) Error() string {
return fmt.Sprintf(
"Value \"%s\" is a invalid type, expected type \"%s\". Got err: %s",
e.value,
e.expected,
e.err.Error(),
)
}
type ErrUnsuportedValueType struct {
supported []string
value string
}
func (e ErrUnsuportedValueType) Error() string {
return fmt.Sprintf(
"Value \"%s\" is a unsupported type, supported types are: \"%s\"",
e.value,
strings.Join(e.supported, ", "),
)
}
type ErrMissingRequiredValue struct {
value string
}
func (e ErrMissingRequiredValue) Error() string {
return fmt.Sprintf("Required value \"%s\" missing from query", e.value)
}
var (
ErrParseForm = errors.New("Failed to parse form from body or query parameters")
ErrReflectPanic = errors.New("Reflect panic while trying to parse request")
)

View File

@@ -1,12 +0,0 @@
package middleware
import (
"net/http"
)
func CacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=604800, stale-while-revalidate=86400, public")
next.ServeHTTP(w, r)
})
}

View File

@@ -1,73 +0,0 @@
package middleware
import (
"log/slog"
"math/rand"
"net/http"
)
func DevMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
type loggerReponse struct {
http.ResponseWriter
status int
}
func (lr *loggerReponse) WriteHeader(s int) {
lr.status = s
lr.ResponseWriter.WriteHeader(s)
}
func NewLoggerMiddleware(l *slog.Logger) Middleware {
l = l.WithGroup("logger_middleware")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := randHash(5)
l.Info("NEW REQUEST",
slog.String("id", id),
slog.String("status", "xxx"),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
)
lw := &loggerReponse{w, http.StatusOK}
next.ServeHTTP(lw, r)
if lw.status >= 400 {
l.Warn("ERR REQUEST",
slog.String("id", id),
slog.Int("status", lw.status),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
)
return
}
l.Info("END REQUEST",
slog.String("id", id),
slog.Int("status", lw.status),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
)
})
}
}
const HASH_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// This is not the most performant function, as a TODO we could
// improve based on this Stackoberflow thread:
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
func randHash(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = HASH_CHARS[rand.Int63()%int64(len(HASH_CHARS))]
}
return string(b)
}

View File

@@ -1,108 +0,0 @@
package middleware
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
)
type Middleware = func(next http.Handler) http.Handler
type MiddlewaredReponse struct {
w http.ResponseWriter
statuses []int
bodyWrites [][]byte
}
func NewMiddlewaredResponse(w http.ResponseWriter) *MiddlewaredReponse {
return &MiddlewaredReponse{w, []int{500}, [][]byte{[]byte("")}}
}
func (m *MiddlewaredReponse) WriteHeader(s int) {
m.Header().Set("Status", strconv.Itoa(s))
m.statuses = append(m.statuses, s)
}
func (m *MiddlewaredReponse) Header() http.Header {
return m.w.Header()
}
func (m *MiddlewaredReponse) Write(b []byte) (int, error) {
m.bodyWrites = append(m.bodyWrites, b)
return len(b), nil
}
func (m *MiddlewaredReponse) ReallyWriteHeader() (int, error) {
status := m.statuses[len(m.statuses)-1]
m.w.WriteHeader(status)
bytes := 0
for _, b := range m.bodyWrites {
by, err := m.w.Write(b)
if err != nil {
return bytes, errors.Join(
fmt.Errorf(
"Failed to write to response in middleware."+
"\nStatuses are %v"+
"\nTried to write %v bytes"+
"\nTried to write response:\n%s",
m.statuses, bytes, string(b),
),
err,
)
}
bytes += by
}
return bytes, nil
}
type multiResponseWriter struct {
response http.ResponseWriter
writers []io.Writer
}
func MultiResponseWriter(
w http.ResponseWriter,
writers ...io.Writer,
) http.ResponseWriter {
if mw, ok := w.(*multiResponseWriter); ok {
mw.writers = append(mw.writers, writers...)
return mw
}
allWriters := make([]io.Writer, 0, len(writers))
for _, iow := range writers {
if mw, ok := iow.(*multiResponseWriter); ok {
allWriters = append(allWriters, mw.writers...)
} else {
allWriters = append(allWriters, iow)
}
}
return &multiResponseWriter{w, allWriters}
}
func (w *multiResponseWriter) WriteHeader(status int) {
w.Header().Set("Status", strconv.Itoa(status))
w.response.WriteHeader(status)
}
func (w *multiResponseWriter) Write(p []byte) (int, error) {
w.WriteHeader(http.StatusOK)
for _, w := range w.writers {
n, err := w.Write(p)
if err != nil {
return n, err
}
if n != len(p) {
return n, io.ErrShortWrite
}
}
return w.response.Write(p)
}
func (w *multiResponseWriter) Header() http.Header {
return w.response.Header()
}

View File

@@ -1,25 +0,0 @@
package router
import (
"net/http"
"forge.capytal.company/capytalcode/project-comicverse/lib/middleware"
)
var DefaultRouter = NewRouter()
func Handle(pattern string, handler http.Handler) {
DefaultRouter.Handle(pattern, handler)
}
func HandleFunc(pattern string, handler http.HandlerFunc) {
DefaultRouter.HandleFunc(pattern, handler)
}
func Use(m middleware.Middleware) {
DefaultRouter.Use(m)
}
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
DefaultRouter.ServeHTTP(w, r)
}

View File

@@ -1,43 +0,0 @@
package rerrors
import (
"net/http"
"strconv"
)
func BadRequest(reason ...string) RouteError {
info := map[string]any{}
if len(reason) == 1 {
info["reason"] = reason[0]
} else if len(reason) > 1 {
for i, r := range reason {
info["reason_"+strconv.Itoa(i)] = r
}
}
return NewRouteError(http.StatusBadRequest, "Bad Request", info)
}
func NotFound() RouteError {
return NewRouteError(http.StatusNotFound, "Not Found", map[string]any{})
}
func MissingCookies(cookies []string) RouteError {
return NewRouteError(http.StatusBadRequest, "Missing cookies", map[string]any{
"missing_cookies": cookies,
})
}
func MethodNowAllowed(method string, allowedMethods []string) RouteError {
return NewRouteError(http.StatusMethodNotAllowed, "Method not allowed", map[string]any{
"method": method,
"allowed_methods": allowedMethods,
})
}
func MissingParameters(params []string) RouteError {
return NewRouteError(http.StatusBadRequest, "Missing parameters", map[string]any{
"missing_parameters": params,
})
}

View File

@@ -1,14 +0,0 @@
package rerrors
import (
"errors"
"net/http"
)
func InternalError(errs ...error) RouteError {
err := errors.Join(errs...)
return NewRouteError(http.StatusInternalServerError, "Internal server error", map[string]any{
"errors": err,
"errors_desc": err.Error(),
})
}

View File

@@ -1,167 +0,0 @@
package rerrors
import (
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"forge.capytal.company/capytalcode/project-comicverse/lib/middleware"
"github.com/a-h/templ"
)
const (
ERROR_MIDDLEWARE_HEADER = "XX-Error-Middleware"
ERROR_VALUE_HEADER = "X-Error-Value"
)
type RouteError struct {
StatusCode int `json:"status_code"`
Error string `json:"error"`
Info map[string]any `json:"info"`
Endpoint string
}
func NewRouteError(status int, error string, info ...map[string]any) RouteError {
rerr := RouteError{StatusCode: status, Error: error}
if len(info) > 0 {
rerr.Info = info[0]
} else {
rerr.Info = map[string]any{}
}
return rerr
}
func (rerr RouteError) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if rerr.StatusCode == 0 {
rerr.StatusCode = http.StatusNotImplemented
}
if rerr.Error == "" {
rerr.Error = "MISSING ERROR DESCRIPTION"
}
if rerr.Info == nil {
rerr.Info = map[string]any{}
}
j, err := json.Marshal(rerr)
if err != nil {
j, _ = json.Marshal(RouteError{
StatusCode: http.StatusInternalServerError,
Error: "Failed to marshal error message to JSON",
Info: map[string]any{
"source_value": fmt.Sprintf("%#v", rerr),
"error": err.Error(),
},
})
}
if r.Header.Get(ERROR_MIDDLEWARE_HEADER) == "enable" && prefersHtml(r.Header) {
q := r.URL.Query()
q.Set("error", base64.URLEncoding.EncodeToString(j))
r.URL.RawQuery = q.Encode()
http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(rerr.StatusCode)
if _, err = w.Write(j); err != nil {
_, _ = w.Write([]byte("Failed to write error JSON string to body"))
}
}
type ErrorMiddlewarePage func(err RouteError) templ.Component
type ErrorDisplayer struct {
log *slog.Logger
page ErrorMiddlewarePage
}
func (h ErrorDisplayer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
e, err := base64.URLEncoding.DecodeString(r.URL.Query().Get("error"))
if err != nil {
h.log.Error("Failed to decode \"error\" parameter from error redirect",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", 0),
slog.String("data", string(e)),
)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(
fmt.Sprintf("Data %s\nError %s", string(e), err.Error()),
))
return
}
var rerr RouteError
if err := json.Unmarshal(e, &rerr); err != nil {
h.log.Error("Failed to decode \"error\" parameter from error redirect",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", 0),
slog.String("data", string(e)),
)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(
fmt.Sprintf("Data %s\nError %s", string(e), err.Error()),
))
return
}
if rerr.Endpoint == "" {
q := r.URL.Query()
q.Del("error")
r.URL.RawQuery = q.Encode()
rerr.Endpoint = r.URL.String()
}
w.WriteHeader(rerr.StatusCode)
if err := h.page(rerr).Render(r.Context(), w); err != nil {
_, _ = w.Write(e)
}
}
func NewErrorMiddleware(
p ErrorMiddlewarePage,
l *slog.Logger,
notfound ...ErrorMiddlewarePage,
) middleware.Middleware {
var nf ErrorMiddlewarePage
if len(notfound) > 0 {
nf = notfound[0]
} else {
nf = p
}
l = l.WithGroup("error_middleware")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Set(ERROR_MIDDLEWARE_HEADER, "enable")
if uerr := r.URL.Query().Get("error"); uerr != "" && prefersHtml(r.Header) {
ErrorDisplayer{l, nf}.ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r)
})
}
}
func prefersHtml(h http.Header) bool {
if h.Get("Accept") == "" {
return false
}
return (strings.Contains(h.Get("Accept"), "text/html") ||
strings.Contains(h.Get("Accept"), "application/xhtml+xml") ||
strings.Contains(h.Get("Accept"), "application/xml")) &&
!strings.Contains(h.Get("Accept"), "application/json")
}

View File

@@ -1,231 +0,0 @@
package router
import (
"fmt"
"net/http"
"path"
"strings"
"forge.capytal.company/capytalcode/project-comicverse/lib/middleware"
)
type Router interface {
Handle(pattern string, handler http.Handler)
HandleFunc(pattern string, handler http.HandlerFunc)
Use(middleware middleware.Middleware)
http.Handler
}
type RouterWithRoutes interface {
Router
Routes() []Route
}
type RouterWithMiddlewares interface {
RouterWithRoutes
Middlewares() []middleware.Middleware
}
type RouterWithMiddlewaresWrapper interface {
RouterWithMiddlewares
WrapMiddlewares(ms []middleware.Middleware, h http.Handler) http.Handler
}
type Route struct {
Path string
Method string
Host string
Handler http.Handler
}
func NewRouter(mux ...*http.ServeMux) Router {
var m *http.ServeMux
if len(mux) > 0 {
m = mux[0]
} else {
m = http.NewServeMux()
}
return &defaultRouter{
m,
[]middleware.Middleware{},
map[string]Route{},
}
}
type defaultRouter struct {
mux *http.ServeMux
middlewares []middleware.Middleware
routes map[string]Route
}
func (r *defaultRouter) Handle(pattern string, h http.Handler) {
if sr, ok := h.(Router); ok {
r.handleRouter(pattern, sr)
} else {
r.handle(pattern, h)
}
}
func (r *defaultRouter) HandleFunc(pattern string, hf http.HandlerFunc) {
r.handle(pattern, hf)
}
func (r *defaultRouter) Use(m middleware.Middleware) {
r.middlewares = append(r.middlewares, m)
}
func (r *defaultRouter) Routes() []Route {
rs := make([]Route, len(r.routes))
i := 0
for _, r := range r.routes {
rs[i] = r
i++
}
return rs
}
func (r *defaultRouter) Middlewares() []middleware.Middleware {
return r.middlewares
}
func (r defaultRouter) WrapMiddlewares(ms []middleware.Middleware, h http.Handler) http.Handler {
hf := h
for _, m := range ms {
hf = m(hf)
}
return hf
}
func (r *defaultRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.mux.ServeHTTP(w, req)
}
func (r defaultRouter) handle(pattern string, hf http.Handler) {
m, h, p := r.parsePattern(pattern)
rt := Route{
Method: m,
Host: h,
Path: p,
Handler: hf,
}
r.handleRoute(rt)
}
func (r defaultRouter) handleRouter(pattern string, rr Router) {
m, h, p := r.parsePattern(pattern)
rs, ok := rr.(RouterWithRoutes)
if !ok {
r.handle(p, rr)
}
routes := rs.Routes()
middlewares := []middleware.Middleware{}
if rw, ok := rs.(RouterWithMiddlewares); ok {
middlewares = rw.Middlewares()
}
wrap := r.WrapMiddlewares
if rw, ok := rs.(RouterWithMiddlewaresWrapper); ok {
wrap = rw.WrapMiddlewares
}
for _, route := range routes {
route.Handler = wrap(middlewares, route.Handler)
route.Path = path.Join(p, route.Path)
if m != "" && route.Method != "" && m != route.Method {
panic(
fmt.Sprintf(
"Nested router's route has incompatible method than defined in path %q."+
"Router's route method is %q, while path's is %q",
p, route.Method, m,
),
)
}
if h != "" && route.Host != "" && h != route.Host {
panic(
fmt.Sprintf(
"Nested router's route has incompatible host than defined in path %q."+
"Router's route host is %q, while path's is %q",
p, route.Host, h,
),
)
}
r.handleRoute(route)
}
}
func (r defaultRouter) handleRoute(rt Route) {
if len(r.middlewares) > 0 {
rt.Handler = r.WrapMiddlewares(r.middlewares, rt.Handler)
}
if rt.Path == "" || !strings.HasPrefix(rt.Path, "/") {
panic(
fmt.Sprintf(
"INVALID STATE: Path of route (%#v) does not start with a leading slash",
rt,
),
)
}
p := rt.Path
if rt.Host != "" {
p = fmt.Sprintf("%s%s", rt.Host, p)
}
if rt.Method != "" {
p = fmt.Sprintf("%s %s", rt.Method, p)
}
if !strings.HasSuffix(p, "/") {
p = fmt.Sprintf("%s/", p)
}
r.routes[p] = rt
r.mux.Handle(p, rt.Handler)
}
func (r *defaultRouter) parsePattern(pattern string) (method, host, p string) {
pattern = strings.TrimSpace(pattern)
// ServerMux patterns are "[METHOD ][HOST]/[PATH]", so to parsing it, we must
// first split it between "[METHOD ][HOST]" and "[PATH]"
ps := strings.Split(pattern, "/")
p = path.Join("/", strings.Join(ps[1:], "/"))
// path.Join adds a trailing slash, if the original pattern doesn't has one, the parsed
// path shouldn't also
if !strings.HasSuffix(pattern, "/") {
p = strings.TrimSuffix(p, "/")
}
// Since path.Join adds a trailing slash, it can break the {pattern...} syntax.
// So we check if it has the suffix "...}/" to see if it ends in "/{pattern...}/"
if strings.HasSuffix(p, "...}/") {
// If it does, we remove the any possible trailing slash
p = strings.TrimSuffix(p, "/")
}
// If "[METHOD ][HOST]" is empty, we just have the path and can send it back
if ps[0] == "" {
return "", "", p
}
// Split string again, if method is not defined, this will end up being just []string{"[HOST]"}
// since there isn't a space before the host. If there is a method defined, this will end up as
// []string{"[METHOD]","[HOST]"}, with "[HOST]" being possibly a empty string.
mh := strings.Split(ps[0], " ")
// If slice is of length 1, this means it is []string{"[HOST]"}
if len(mh) == 1 {
return "", host, p
}
return mh[0], mh[1], p
}

39
main.go
View File

@@ -1,49 +1,18 @@
package main
package capytalcodecomicverse
import (
"flag"
"net/http"
"os"
"strconv"
"forge.capytal.company/capytalcode/project-comicverse/app"
)
var (
port *int
dev *bool
debug = flag.Bool("dev", false, "Run the server in debug mode.")
port = flag.Int("port", 8080, "Port to be used for the server.")
)
func init() {
portEnv := os.Getenv("COMICVERSE_PORT")
if portEnv == "" {
portEnv = "8080"
}
p, err := strconv.Atoi(portEnv)
if err != nil {
p = 8080
}
port = flag.Int("port", p, "The port to the server to listen on")
devEnv := os.Getenv("COMICVERSE_DEV")
if devEnv == "" {
devEnv = "false"
}
d, err := strconv.ParseBool(devEnv)
if err != nil {
d = false
}
dev = flag.Bool("dev", d, "Run the application in development mode")
flag.Parse()
}
func main() {
flag.Parse()
app := app.NewApp(app.AppOpts{
Port: port,
Dev: dev,
Assets: http.StripPrefix("/assets/", http.FileServer(http.Dir("./assets"))),
})
app.Run()
}

6700
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://json.schemastore.org/package.json",
"type": "module",
"devDependencies": {
"@eslint/js": "^9.12.0",
"@stylistic/eslint-plugin": "^2.9.0",
"@types/eslint__js": "^8.42.3",
"eslint": "^9.12.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsdoc": "^50.3.2",
"eslint-plugin-json": "^4.0.1",
"eslint-plugin-perfectionist": "^3.8.0",
"eslint-plugin-unicorn": "^56.0.0",
"eslint-plugin-wc": "^2.2.0",
"globals": "^15.11.0",
"typescript": "^4.9.5",
"typescript-eslint": "^8.8.1",
"unocss": "^0.63.3"
}
}

View File

@@ -1,94 +0,0 @@
package layouts
import (
"fmt"
"embed"
"forge.capytal.company/capytalcode/project-comicverse/configs"
"forge.capytal.company/capytalcode/project-comicverse/assets"
)
type PageInfo struct {
Title string
Description string
Author string
Keywords string
ThemeColor string
Heading templ.Component
}
func pageInfo(info []PageInfo) PageInfo {
if len(info) != 0 {
return info[0]
}
return PageInfo{}
}
templ LinkCSSFile(href string, fs embed.FS, file string) {
if configs.DEVELOPMENT {
<link rel="preload" href={ href } as="style"/>
<link rel="stylesheet" href={ href }/>
} else if f, err := fs.ReadFile(file); err != nil {
<link rel="preload" href={ href } as="style"/>
<link rel="stylesheet" href={ href }/>
} else {
@templ.Raw(fmt.Sprintf("<style>%s</style>", f))
}
}
templ Page(i ...PageInfo) {
<html lang="en-US">
<head>
<meta charset="utf-8"/>
// Page information
if pageInfo(i).Title != "" {
<title>{ pageInfo(i).Title + " - " + configs.APP_NAME }</title>
} else {
<title>Comicverse</title>
}
if pageInfo(i).Author != "" {
<meta name="author" content={ pageInfo(i).Author }/>
} else {
<meta name="author" content={ configs.APP_NAME }/>
}
if pageInfo(i).Description != "" {
<meta name="description" content={ pageInfo(i).Description }/>
}
<meta name="publisher" content={ configs.APP_NAME }/>
// Page configuration
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1"/>
<meta name="referrer" content="strict-origin-when-cross-origin"/>
<meta name="color-scheme" content="dark light"/>
if pageInfo(i).ThemeColor != "" {
<meta name="theme-color" content={ pageInfo(i).ThemeColor }/>
}
// Global styles
<link rel="preload" href="/assets/fonts/KarlaVF.woff2" as="font"/>
<link rel="preload" href="/assets/fonts/KarlaItalicVF.woff2" as="font"/>
<link rel="preload" href="/assets/fonts/PlayfairRomanVF.woff2" as="font"/>
<link rel="preload" href="/assets/fonts/PlayfairItalicVF.woff2" as="font"/>
@LinkCSSFile("/assets/css/theme.css", assets.ASSETS, "css/theme.css")
@LinkCSSFile("/assets/css/uno.css", assets.ASSETS, "css/uno.css")
// Global scripts
<script type="module" src="/assets/lib/entry.js" defer></script>
if configs.DEVELOPMENT {
<script type="module">
import htmx from '/assets/lib/htmx.js'; htmx.logAll(); window.htmx = htmx;
</script>
} else {
<script type="module">
import htmx from '/assets/lib/htmx.js'; htmx.logNone(); window.htmx = htmx;
</script>
}
// Additional heading
if pageInfo(i).Heading != nil {
@pageInfo(i).Heading
}
</head>
<body style="--accent-color:#111">
<main class="absolute w-screen min-h-screen top-0 left-0 bg-neutral-10">
{ children... }
</main>
</body>
</html>
}

0
templates/templates.go Normal file
View File

View File

@@ -1,26 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"alwaysStrict": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ES2022",
"moduleResolution": "Node16",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "ESNext"
},
"include": [
"./assets/**/*",
"./eslint.config.js",
"./uno.config.js",
"**/*.json"
],
"exclude": [
"./node_modules/**",
"./dist"
]
}

View File

@@ -1,115 +0,0 @@
import {
defineConfig,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from 'unocss';
export default defineConfig({
cli: {
entry: {
outFile: './assets/css/uno.css',
patterns: [
'./{handlers,templates}/**/*.templ',
'./assets/**/*.{js,css,html}',
'!./assets/css/uno.css',
],
},
},
presets: [
presetIcons(),
presetTypography(),
presetUno({
dark: 'media',
}),
presetWebFonts({
fonts: {
display: {
name: 'Playfair',
},
sans: {
name: 'Karla',
},
},
provider: 'none',
}),
],
theme: {
colors: {
accent: {
'10': 'var(--theme-accent-10)',
'20': 'var(--theme-accent-20)',
'30': 'var(--theme-accent-30)',
'40': 'var(--theme-accent-40)',
'50': 'var(--theme-accent-50)',
'60': 'var(--theme-accent-60)',
'70': 'var(--theme-accent-70)',
'80': 'var(--theme-accent-80)',
'90': 'var(--theme-accent-90)',
'100': 'var(--theme-accent-100)',
'110': 'var(--theme-accent-110)',
'120': 'var(--theme-accent-120)',
},
danger: {
'10': 'var(--theme-danger-10)',
'20': 'var(--theme-danger-20)',
'30': 'var(--theme-danger-30)',
'40': 'var(--theme-danger-40)',
'50': 'var(--theme-danger-50)',
'60': 'var(--theme-danger-60)',
'70': 'var(--theme-danger-70)',
'80': 'var(--theme-danger-80)',
'90': 'var(--theme-danger-90)',
'100': 'var(--theme-danger-100)',
'110': 'var(--theme-danger-110)',
'120': 'var(--theme-danger-120)',
},
neutral: {
'10': 'var(--theme-neutral-10)',
'20': 'var(--theme-neutral-20)',
'30': 'var(--theme-neutral-30)',
'40': 'var(--theme-neutral-40)',
'50': 'var(--theme-neutral-50)',
'60': 'var(--theme-neutral-60)',
'70': 'var(--theme-neutral-70)',
'80': 'var(--theme-neutral-80)',
'90': 'var(--theme-neutral-90)',
'100': 'var(--theme-neutral-100)',
'110': 'var(--theme-neutral-110)',
'120': 'var(--theme-neutral-120)',
},
success: {
'10': 'var(--theme-success-10)',
'20': 'var(--theme-success-20)',
'30': 'var(--theme-success-30)',
'40': 'var(--theme-success-40)',
'50': 'var(--theme-success-50)',
'60': 'var(--theme-success-60)',
'70': 'var(--theme-success-70)',
'80': 'var(--theme-success-80)',
'90': 'var(--theme-success-90)',
'100': 'var(--theme-success-100)',
'110': 'var(--theme-success-110)',
'120': 'var(--theme-success-120)',
},
warn: {
'10': 'var(--theme-warn-10)',
'20': 'var(--theme-warn-20)',
'30': 'var(--theme-warn-30)',
'40': 'var(--theme-warn-40)',
'50': 'var(--theme-warn-50)',
'60': 'var(--theme-warn-60)',
'70': 'var(--theme-warn-70)',
'80': 'var(--theme-warn-80)',
'90': 'var(--theme-warn-90)',
'100': 'var(--theme-warn-100)',
'110': 'var(--theme-warn-110)',
'120': 'var(--theme-warn-120)',
},
},
},
transformers: [transformerDirectives(), transformerVariantGroup()],
});

2
x

Submodule x updated: f6a044a2b6...90a5169f1b