Initial commit
44
.air.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main cmd/main.go"
|
||||||
|
delay = 0
|
||||||
|
exclude_dir = ["node_modules", "assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = ["cmd", "internal", "views", "pkg"]
|
||||||
|
include_ext = ["go", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/tmp/
|
||||||
|
/views/**/*.go
|
||||||
|
|
||||||
43
cmd/main.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
|
||||||
|
"hugo.mardbrink.se/database"
|
||||||
|
"hugo.mardbrink.se/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Template struct {
|
||||||
|
template *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTemplate() *Template {
|
||||||
|
return &Template{
|
||||||
|
template: template.Must(template.ParseGlob("views/*.html")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||||
|
return t.template.ExecuteTemplate(w, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
e := echo.New()
|
||||||
|
|
||||||
|
e.Renderer = newTemplate()
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
|
||||||
|
e.Static("/resources", "resources")
|
||||||
|
e.Static("/css", "css")
|
||||||
|
|
||||||
|
db := database.InitDB()
|
||||||
|
defer database.CloseDB(db)
|
||||||
|
|
||||||
|
handlers.RegisterRoutes(e, db)
|
||||||
|
|
||||||
|
e.Logger.Fatal(e.Start(":8080"))
|
||||||
|
}
|
||||||
485
css/index.css
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
:root {
|
||||||
|
--primary: #F6F6F6;
|
||||||
|
--accent: #201F1B;
|
||||||
|
--light-text: #F6F6F6;
|
||||||
|
--dark-text: #201F1B;
|
||||||
|
|
||||||
|
font-family: "Archivo Black", serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--dark-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-breadcrumbs {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-breadcrumbs-link {
|
||||||
|
color: var(--dark-text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-breadcrumbs-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grain-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-padding {
|
||||||
|
margin-top: 5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
margin-bottom: -3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
font-size: 12.5vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container {
|
||||||
|
margin-top: 3rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
margin-top: 7.5rem;
|
||||||
|
gap: 1.5rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
background-color: var(--dark-text);
|
||||||
|
width: 0.3rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
width: 6rem;
|
||||||
|
height: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 2.5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 11vw;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modmark-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
margin-right: 1rem;
|
||||||
|
height: 15rem;
|
||||||
|
width: 22.5rem;
|
||||||
|
border: 0.4rem solid var(--dark-text);
|
||||||
|
box-shadow: 0.5rem 0.5rem 0 0.1rem var(--dark-text);
|
||||||
|
margin-top: 5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modmark-text {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modmark-icon-box {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: -3.5rem;
|
||||||
|
right: -1.75rem;
|
||||||
|
height: 5.5rem;
|
||||||
|
width: 5.5rem;
|
||||||
|
border: 0.4rem solid var(--dark-text);
|
||||||
|
box-shadow: 0.5rem 0.5rem 0 0.1rem var(--dark-text);
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modmark-mobile-icon-box {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: -2.5rem;
|
||||||
|
right: -1.25rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
width: 3.5rem;
|
||||||
|
border: 0.4rem solid var(--dark-text);
|
||||||
|
box-shadow: 0.5rem 0.5rem 0 0.1rem var(--dark-text);
|
||||||
|
background-color: var(--primary);
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modmark-icon {
|
||||||
|
width: 3.75rem;
|
||||||
|
height: 3.75rem;
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 0;
|
||||||
|
margin-right: 1rem;
|
||||||
|
height: 15rem;
|
||||||
|
width: 28rem;
|
||||||
|
border: 0.4rem solid var(--dark-text);
|
||||||
|
box-shadow: 0.5rem 0.5rem 0 0.1rem var(--dark-text);
|
||||||
|
margin-top: 5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
width: 18rem;
|
||||||
|
height: auto;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info-title {
|
||||||
|
margin-left: 1rem;
|
||||||
|
font-size: 2.20rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info-desc {
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-family: "Archivo", serif;
|
||||||
|
font-weight: 400;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info-link-left {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
align-self: flex-end;
|
||||||
|
justify-self: flex-end;
|
||||||
|
margin-right: 1.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: ">";
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info-link-right {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
align-self: flex-start;
|
||||||
|
justify-self: flex-end;
|
||||||
|
margin-left: 1.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "<";
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
font-size: 1rem;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-right: 1.75rem;
|
||||||
|
&:after {
|
||||||
|
content: ">";
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info-link-right:hover, .project-info-link-left:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skal-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-right: 1rem;
|
||||||
|
height: 15rem;
|
||||||
|
width: 22.5rem;
|
||||||
|
border: 0.4rem solid var(--dark-text);
|
||||||
|
box-shadow: 0.5rem 0.5rem 0 0.1rem var(--dark-text);
|
||||||
|
margin-top: 5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skal-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5rem;
|
||||||
|
height: 0.4rem;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--dark-text);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skal-bar-mobile {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
height: 0.4rem;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--dark-text);
|
||||||
|
gap: 0.5rem;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skal-text {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skal-link-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skal-mobile-title-offset {
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-left: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
height: 0.7rem;
|
||||||
|
width: 0.7rem;
|
||||||
|
background-color: var(--dark-text);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-wrapper-mobile {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-left: 0.3rem;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-mobile {
|
||||||
|
height: 0.5rem;
|
||||||
|
width: 0.5rem;
|
||||||
|
background-color: var(--dark-text);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dct-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-right: 1rem;
|
||||||
|
height: 15rem;
|
||||||
|
width: 22.5rem;
|
||||||
|
border: 0.4rem solid var(--dark-text);
|
||||||
|
box-shadow: 0.5rem 0.5rem 0 0.1rem var(--dark-text);
|
||||||
|
margin-top: 5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dct-satellite {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dct-earth {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2.5rem;
|
||||||
|
right: -1.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dct-mobile-satellite {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2.1rem;
|
||||||
|
left: -1.6rem;
|
||||||
|
height: 4rem;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
database/data.db
Normal file
44
database/database.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
|
"hugo.mardbrink.se/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitDB() *sql.DB {
|
||||||
|
db, err := sql.Open("sqlite3", config.DatabaseFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initSchema(db); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSchema(db *sql.DB) error {
|
||||||
|
content, err := os.ReadFile(config.SeedFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseDB(db *sql.DB) {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
7
database/seed.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS articles (
|
||||||
|
route TEXT PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
description TEXT,
|
||||||
|
content TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
22
go.mod
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
module hugo.mardbrink.se
|
||||||
|
|
||||||
|
go 1.22.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
golang.org/x/crypto v0.22.0 // indirect
|
||||||
|
golang.org/x/net v0.24.0 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
)
|
||||||
37
go.sum
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||||
|
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
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=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
50
internal/auth/auth.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"crypto/sha512"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/http"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
envUsernameHash = "HUGOMARDBRINK_USERNAME"
|
||||||
|
envPasswordHash = "HUGOMARDBRINK_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BasicAuth(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return echo.HandlerFunc(func(c echo.Context) error {
|
||||||
|
username, password, ok := c.Request().BasicAuth()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
c.Response().Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameHash := sha512.Sum512([]byte(username))
|
||||||
|
passwordHash := sha512.Sum512([]byte(password))
|
||||||
|
|
||||||
|
expectedUsernameHash, err := hex.DecodeString(os.Getenv(envUsernameHash))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
expectedPasswordHash, err := hex.DecodeString(os.Getenv(envPasswordHash))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1
|
||||||
|
passwordMatch := subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1
|
||||||
|
|
||||||
|
if usernameMatch && passwordMatch {
|
||||||
|
return next(c)
|
||||||
|
} else {
|
||||||
|
c.Response().Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
6
internal/config/config.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
DatabaseFile = "./database/data.db"
|
||||||
|
SeedFile = "./database/seed.sql"
|
||||||
|
)
|
||||||
67
internal/handlers/handlers.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
|
"hugo.mardbrink.se/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(e *echo.Echo, db *sql.DB) {
|
||||||
|
e.GET("/", homePageHandler())
|
||||||
|
e.GET("/projects", projectsPageHandler())
|
||||||
|
e.GET("/articles", articlesPageHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
func homePageHandler() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
isHtmx := c.Request().Header.Get("HX-Request") == "true"
|
||||||
|
c.Request().Header.Add("Vary", "HX-Request")
|
||||||
|
p := models.Page{
|
||||||
|
Title: "Hugo Mårdbrink",
|
||||||
|
Description: "Home page of Hugo Mårdbrinks personal website.",
|
||||||
|
IsHtmx: isHtmx,
|
||||||
|
Breadcrumbs: models.Breadcrumbs{models.NewBreadcrumb("home", "/")}}
|
||||||
|
hp := models.NewHomePage(p)
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "home", hp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectsPageHandler() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
isHtmx := c.Request().Header.Get("HX-Request") == "true"
|
||||||
|
c.Request().Header.Add("Vary", "HX-Request")
|
||||||
|
|
||||||
|
p := models.Page{
|
||||||
|
Title: "Hugo Mårdbrink - Projects",
|
||||||
|
Description: "Hobby projects by Hugo Mårdbrink.",
|
||||||
|
IsHtmx: isHtmx,
|
||||||
|
Breadcrumbs: models.Breadcrumbs{
|
||||||
|
models.NewBreadcrumb("Home", "/"),
|
||||||
|
models.NewBreadcrumb("projects", "/projects")}}
|
||||||
|
pp := models.NewProjectPage(p)
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "projects", pp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func articlesPageHandler() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
isHtmx := c.Request().Header.Get("HX-Request") == "true"
|
||||||
|
c.Request().Header.Add("Vary", "HX-Request")
|
||||||
|
|
||||||
|
p := models.Page{
|
||||||
|
Title: "Hugo Mårdbrink - Articles",
|
||||||
|
Description: "Articles by Hugo Mårdbrink.",
|
||||||
|
IsHtmx: isHtmx,
|
||||||
|
Breadcrumbs: models.Breadcrumbs{
|
||||||
|
models.NewBreadcrumb("Home", "/"),
|
||||||
|
models.NewBreadcrumb("articles", "/articles")}}
|
||||||
|
pp := models.NewProjectPage(p)
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "articles", pp)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
internal/models/pages.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Breadcrumb struct {
|
||||||
|
Title string
|
||||||
|
Route string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Breadcrumbs = []Breadcrumb
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Breadcrumbs Breadcrumbs
|
||||||
|
IsHtmx bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type HomePage struct {
|
||||||
|
Page Page
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectPage struct {
|
||||||
|
Page Page
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func NewPage(title string, description string, breadcrumbs Breadcrumbs, isHtmx bool) Page {
|
||||||
|
return Page{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Breadcrumbs: breadcrumbs,
|
||||||
|
IsHtmx: isHtmx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBreadcrumb(title string, route string) Breadcrumb {
|
||||||
|
return Breadcrumb{
|
||||||
|
Title: title,
|
||||||
|
Route: route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHomePage(page Page) HomePage {
|
||||||
|
return HomePage{
|
||||||
|
Page: page,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectPage(page Page) ProjectPage {
|
||||||
|
return ProjectPage{
|
||||||
|
Page: page,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
24402
resources/MARDBRINK_HUGO_CV.pdf
Normal file
5
resources/earth.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="291" height="278" viewBox="0 0 291 278" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22.8781 243.216C29.0761 139.095 203.238 34.1208 271.33 57.5033" stroke="#201F1B" stroke-width="8"/>
|
||||||
|
<path d="M117.52 152.395L104.607 153.662L89.2997 184.529L82.7967 190.61L90.2481 198.579L98.8362 194.569L105.193 199.54L131.207 182.109L142.373 186.604L152.719 179.227L147.202 168.875L160.975 159.443L180.879 148.3L196.199 150.059L205.562 144.176L203.863 135.999L221.894 126.032L230.675 131.608L252.869 105.11L252.887 80.9656L239.644 85.88L235.396 74.342L226.35 79.9286L222.491 72.6222L209.699 80.5621L207.459 88.976L190.983 98.0629L183.195 96.7285L156.232 121.942L151.073 116.425L143.533 117.263L129.892 130.018L129.892 140.827L117.52 152.395Z" stroke="#201F1B" stroke-width="8"/>
|
||||||
|
<path d="M128.799 197.53L124.713 199.095L117.347 208.802L125.046 207.803L134.577 204.808L139.469 196.852L135.921 194.815L128.799 197.53Z" stroke="#201F1B" stroke-width="6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 971 B |
BIN
resources/grain.png
Normal file
|
After Width: | Height: | Size: 499 KiB |
3
resources/icon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="132" height="131" viewBox="0 0 132 131" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M89.0843 131V81.41H42.9143V131H0.924258V0.279996H42.9143V47.97H89.0843V0.279996H131.074V131H89.0843Z" fill="#201F1B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 235 B |
6
resources/mobile-satellite.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="67" height="68" viewBox="0 0 67 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M63.2639 46.2476L49.1193 59.2449L37.8603 46.1157L44.6472 39.8793L52.118 33.0145L63.2639 46.2476Z" fill="#F6F6F6"/>
|
||||||
|
<path d="M36.3835 16.6311L21.9098 29.9309L10.837 16.8154L24.9816 3.81813L36.3835 16.6311Z" fill="#F6F6F6"/>
|
||||||
|
<path d="M56.9329 24.0924C58.1576 25.451 58.0552 27.5589 56.7041 28.8004L32.5903 50.9583C31.2392 52.1997 29.1511 52.1048 27.9264 50.7461L17.1555 38.7972C15.9308 37.4386 16.0332 35.3308 17.3843 34.0893L41.4981 11.9314C42.8492 10.6899 44.9373 10.7849 46.162 12.1435L56.9329 24.0924Z" fill="#F6F6F6"/>
|
||||||
|
<path d="M2.73717 42.6588C1.09283 55.5012 12.3759 66.1694 23.0732 64.9339M12.2946 43.6006C12.0509 50.6019 17.0031 55.7594 23.7161 55.8919M63.2639 46.2476L49.1193 59.2449L37.8603 46.1157L44.6472 39.8793L52.118 33.0145L63.2639 46.2476ZM36.3835 16.6311L21.9098 29.9309L10.837 16.8154L24.9816 3.81813L36.3835 16.6311ZM41.4981 11.9314L17.3843 34.0893C16.0332 35.3308 15.9308 37.4386 17.1555 38.7972L27.9264 50.7461C29.1511 52.1048 31.2392 52.1997 32.5903 50.9583L56.7041 28.8004C58.0552 27.5589 58.1576 25.451 56.9329 24.0924L46.162 12.1435C44.9373 10.7849 42.8492 10.6899 41.4981 11.9314Z" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
resources/modmark.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
9
resources/modmark.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
3
resources/satellite.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="139" height="138" viewBox="0 0 139 138" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M95.3215 133.255C122.428 131.156 139.568 103.141 132.48 81.4531M93.1955 113C107.696 110.5 116.196 98 113.614 84.0001M76.9729 6L109.711 29.7978L87.5 58.8201L71.7914 47.4013L54.5 34.8319L76.9729 6ZM27.5 74.5469L61 98.8986L38.7381 127.528L6 103.73L27.5 74.5469ZM15.6621 65.9418L71.4744 106.513C74.6015 108.786 78.9793 108.093 81.2524 104.966L101.244 77.4646C103.517 74.3375 102.825 69.9598 99.6976 67.6866L43.8853 27.1159C40.7582 24.8427 36.3804 25.535 34.1073 28.6621L14.1159 56.1638C11.8428 59.2909 12.535 63.6687 15.6621 65.9418Z" stroke="#201F1B" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 706 B |
21
views/articles.html
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{ block "articles" . }}
|
||||||
|
{{ if .Page.IsHtmx }}
|
||||||
|
{{ template "breadcrumb" . }}
|
||||||
|
{{ template "articles.content" . }}
|
||||||
|
{{ template "oob.head" .Page }}
|
||||||
|
{{ else }}
|
||||||
|
{{ template "base.start" .}}
|
||||||
|
{{ template "articles.content" . }}
|
||||||
|
{{ template "base.end" .}}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
{{ block "articles.content" . }}
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="title-container">
|
||||||
|
<span class="title">ARTICLES</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
64
views/common.html
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
{{ block "base.start" . }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ .Page.Title }}</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta id="meta-description" name="description" content="{{ .Page.Description }}">
|
||||||
|
<script src="https://unpkg.com/htmx.org/dist/htmx.min.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="resources/icon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="resources/icon.svg">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Archivo:ital,wght@0,100..900;1,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/index.css">
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
const handleFooterLinkClick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
window.scrollTo({ top: 0, behavior: 'auto' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<body>
|
||||||
|
<img src="resources/grain.png" class="grain-bg" alt="Grainy background"/>
|
||||||
|
{{ template "breadcrumb" . }}
|
||||||
|
<div id="page">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block "base.end" . }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block "oob.head" . }}
|
||||||
|
<title htmx-oob-swap="true">{{ .Title }}</title>
|
||||||
|
<meta id="meta-description" name="description" content="{{ .Description }}" hx-swap-oob="true">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
{{ block "breadcrumb" . }}
|
||||||
|
<div id="breadcrumbs" hx-swap-oob="{{ .Page.IsHtmx }}" class="header-breadcrumbs">
|
||||||
|
{{ range .Page.Breadcrumbs }}
|
||||||
|
{{ if eq .Title "home" }} {{ else }}
|
||||||
|
<span class="header-breadcrumbs-link"> / </span>
|
||||||
|
<a onclick="handleFooterLinkClick(event)"
|
||||||
|
href="{{ .Route }}"
|
||||||
|
tabindex="0"
|
||||||
|
hx-boost="true"
|
||||||
|
hx-push-url="true"
|
||||||
|
hx-get="{{ .Route }}"
|
||||||
|
hx-target="#page"
|
||||||
|
class="header-breadcrumbs-link">
|
||||||
|
{{ if eq .Title "" }} {{ else }} {{ .Title }} {{ end }}
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
34
views/home.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{{ block "home" . }}
|
||||||
|
{{ if .Page.IsHtmx }}
|
||||||
|
{{ template "breadcrumb" . }}
|
||||||
|
{{ template "home.content" . }}
|
||||||
|
{{ template "oob.head" .Page }}
|
||||||
|
{{ else }}
|
||||||
|
{{ template "base.start" .}}
|
||||||
|
{{ template "home.content" . }}
|
||||||
|
{{ template "base.end" .}}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
{{ block "home.content" . }}
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="home-padding"></div>
|
||||||
|
<div class="title-container">
|
||||||
|
<span class="title">HUGO</span>
|
||||||
|
<span class="title">MÅRDBRINK</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-container">
|
||||||
|
<a href="/projects" class="menu-item">Projects</a>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<a href="https://github.com/hugomardbrink" target="_blank" rel="noopener norefferer" class="menu-item">Github</a>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<a href="https://www.linkedin.com/in/hugomardbrink/" target="_blank" rel="noopener norefferer" class="menu-item">LinkedIn</a>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<a href="/articles" class="menu-item">Articles</a>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<a href="resources/MARDBRINK_HUGO_CV.pdf" download class="menu-item">CV</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
96
views/projects.html
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
{{ block "projects" . }}
|
||||||
|
{{ if .Page.IsHtmx }}
|
||||||
|
{{ template "breadcrumb" . }}
|
||||||
|
{{ template "projects.content" . }}
|
||||||
|
{{ template "oob.head" .Page }}
|
||||||
|
{{ else }}
|
||||||
|
{{ template "base.start" .}}
|
||||||
|
{{ template "projects.content" . }}
|
||||||
|
{{ template "base.end" .}}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
{{ block "projects.content" . }}
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="project-page">
|
||||||
|
<div class="title-container">
|
||||||
|
<span class="title">PROJECTS</span>
|
||||||
|
</div>
|
||||||
|
{{ template "projects.skal" . }}
|
||||||
|
{{ template "projects.dct" . }}
|
||||||
|
{{ template "projects.modmark" . }}
|
||||||
|
<span class="title"> <br> </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block "projects.modmark" . }}
|
||||||
|
<div class="project-wrapper">
|
||||||
|
<div class="project-info">
|
||||||
|
<div class="modmark-mobile-icon-box">
|
||||||
|
<img src="/resources/modmark.svg" class="modmark-icon" alt="Modmark logo."></img>
|
||||||
|
</div>
|
||||||
|
<span class="project-info-title">ModMark</span>
|
||||||
|
<span class="project-info-desc">A modular markup language built in Rust.
|
||||||
|
The language utilises WebAssembly programs for easy package development.</span>
|
||||||
|
<a href="https://modmark.org" target="_blank" rel="noopener norefferer" class="project-info-link-left">Learn more</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modmark-wrapper">
|
||||||
|
<div class="modmark-icon-box">
|
||||||
|
<img src="/resources/modmark.svg" class="modmark-icon" alt="Modmark logo."></img>
|
||||||
|
</div>
|
||||||
|
<span class="modmark-text">[config]<br>import catalog:prettify</span>
|
||||||
|
<span class="modmark-text"># [prettify](Header)<br>[link](modmark.org)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block "projects.skal" . }}
|
||||||
|
<div class="project-wrapper">
|
||||||
|
<div class="project-info">
|
||||||
|
<div class="dot-wrapper-mobile">
|
||||||
|
<span class="dot-mobile"></span>
|
||||||
|
<span class="dot-mobile"></span>
|
||||||
|
<span class="dot-mobile"></span>
|
||||||
|
</div>
|
||||||
|
<div class="skal-bar-mobile"></div>
|
||||||
|
<span class="project-info-title skal-mobile-title-offset">Skal / Zkal</span>
|
||||||
|
<span class="project-info-desc">Lightweight performant UNIX shell built from scratch in C++. Experimented with more explanatory syntax and custom operators. Also available in Zig as Zkal.</span>
|
||||||
|
<div class="skal-link-row">
|
||||||
|
<a href="https://github.com/hugomardbrink/skal" target="_blank" rel="noopener norefferer" class="project-info-link-left">Skal</a>
|
||||||
|
<a href="https://github.com/hugomardbrink/zkal" target="_blank" rel="noopener norefferer" class="project-info-link-left">Zkal</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="skal-wrapper">
|
||||||
|
<div class="dot-wrapper">
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="skal-bar"></div>
|
||||||
|
<span class="skal-text">
|
||||||
|
skal> ls -a |> wc<br>
|
||||||
|
10 10 68<br>
|
||||||
|
skal>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block "projects.dct" . }}
|
||||||
|
<div class="project-wrapper">
|
||||||
|
<div class="dct-wrapper">
|
||||||
|
<img src="/resources/satellite.svg" class="dct-satellite" alt="Satellite"></img>
|
||||||
|
<img src="/resources/earth.svg" class="dct-earth" alt="Earth"></img>
|
||||||
|
</div>
|
||||||
|
<div class="project-info">
|
||||||
|
<img src="/resources/mobile-satellite.svg" class="dct-mobile-satellite" alt="Satellite"></img>
|
||||||
|
<span class="project-info-title">Resource optimised DCT-II algorithm</span>
|
||||||
|
<span class="project-info-desc">Co-designed a satellite version of the DCT-II algorithm with RISC-V hardware and C software. Focusing on energy efficiency using vector instructions, cache coherence and parallelism.</span>
|
||||||
|
<a href="https://github.com/hugomardbrink/resource-optimised-DCT" target="_blank" rel="noopener norefferer" class="project-info-link-right">Learn more</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||