Added a basic homepage

This commit is contained in:
CamZawacki 2026-05-20 16:09:54 +01:00
parent d40640a648
commit 12fd3c04ca
113 changed files with 414 additions and 506 deletions

View file

@ -0,0 +1,66 @@
package components
import (
"github.com/camzawacki/personal-site/internal/msg"
"github.com/camzawacki/personal-site/internal/ui"
"github.com/camzawacki/personal-site/internal/ui/icons"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func FlashMessages(r *ui.Request) Node {
var g Group
var color Color
for _, typ := range []msg.Type{
msg.TypeSuccess,
msg.TypeInfo,
msg.TypeWarning,
msg.TypeError,
} {
for _, str := range msg.Get(r.Context, typ) {
switch typ {
case msg.TypeSuccess:
color = ColorSuccess
case msg.TypeInfo:
color = ColorInfo
case msg.TypeWarning:
color = ColorWarning
case msg.TypeError:
color = ColorError
}
g = append(g, Alert(color, str))
}
}
return g
}
func Alert(color Color, text string) Node {
var class string
switch color {
case ColorSuccess:
class = "alert-success"
case ColorInfo:
class = "alert-info"
case ColorWarning:
class = "alert-warning"
case ColorError:
class = "alert-error"
}
return Div(
Role("alert"),
Class("alert mb-2 "+class),
Attr("x-data", "{show: true}"),
Attr("x-show", "show"),
Span(
Attr("@click", "show = false"),
Class("cursor-pointer"),
icons.XCircle(),
),
Span(Text(text)),
)
}

View file

@ -0,0 +1,121 @@
package components
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type (
CardParams struct {
Title string
Body Group
Footer Group
Color Color
Size Size
}
Stat struct {
Title string
Value string
Description string
Icon Node
}
)
func Badge(color Color, text string) Node {
var class string
switch color {
case ColorSuccess:
class = "badge-success"
case ColorWarning:
class = "badge-warning"
}
return Div(
Class("badge "+class),
Text(text),
)
}
func Divider(text string) Node {
return Div(
Class("divider"),
Text(text),
)
}
func Card(params CardParams) Node {
var colorClass, sizeClass string
switch params.Color {
case ColorSuccess:
colorClass = "bg-success text-success-content"
case ColorPrimary:
colorClass = "bg-primary text-primary-content"
case ColorAccent:
colorClass = "bg-accent text-accent-content"
case ColorNeutral:
colorClass = "bg-neutral text-neutral-content"
case ColorWarning:
colorClass = "bg-warning text-warning-content"
case ColorInfo:
colorClass = "bg-info text-info-content"
}
switch params.Size {
case SizeSmall:
sizeClass = "card-sm"
case SizeMedium:
sizeClass = "card-md"
case SizeLarge:
sizeClass = "card-lg"
}
return Div(
Class("cards mb-2 "+colorClass+" "+sizeClass),
Div(
Class("card-body"),
If(len(params.Title) > 0, Span(
Class("card-title"),
Text(params.Title),
)),
params.Body,
If(params.Footer != nil, Div(
Class("card-actions justify-end"),
params.Footer,
)),
),
)
}
func Stats(stats ...Stat) Node {
g := make(Group, 0, len(stats))
for _, stat := range stats {
g = append(g, Div(
Class("stat"),
Iff(stat.Icon != nil, func() Node {
return Div(
Class("stat-figure text-secondary"),
stat.Icon,
)
}),
Div(
Class("stat-title"),
Text(stat.Title),
),
Div(
Class("stat-value"),
Text(stat.Value),
),
Div(
Class("stat-desc"),
Text(stat.Description),
),
))
}
return Div(
Class("stats shadow"),
g,
)
}

View file

@ -0,0 +1,268 @@
package components
import (
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type (
InputFieldParams struct {
Form form.Form
FormField string
Name string
InputType string
Label string
Value string
Placeholder string
Help string
}
FileFieldParams struct {
Name string
Label string
Help string
}
OptionsParams struct {
Form form.Form
FormField string
Name string
Label string
Value string
Options []Choice
Help string
}
Choice struct {
Value string
Label string
}
TextareaFieldParams struct {
Form form.Form
FormField string
Name string
Label string
Value string
Help string
}
CheckboxParams struct {
Form form.Form
FormField string
Name string
Label string
Checked bool
}
)
func ControlGroup(controls ...Node) Node {
return Div(
Class("mt-2 flex gap-2"),
Group(controls),
)
}
func TextareaField(el TextareaFieldParams) Node {
return Fieldset(
el.Label,
Textarea(
Class("textarea h-24 w-2/3 "+formFieldStatusClass(el.Form, el.FormField)),
ID(el.Name),
Name(el.Name),
Text(el.Value),
),
Help(el.Help),
formFieldErrors(el.Form, el.FormField),
)
}
func Radios(el OptionsParams) Node {
buttons := make(Group, len(el.Options))
for i, opt := range el.Options {
id := "radio-" + el.Name + "-" + opt.Value
buttons[i] = Div(
Class("mb-2"),
Input(
ID(id),
Type("radio"),
Name(el.Name),
Value(opt.Value),
Class("radio mr-1 "+formFieldStatusClass(el.Form, el.FormField)),
If(el.Value == opt.Value, Checked()),
),
Label(
Text(opt.Label),
For(id),
),
)
}
return Fieldset(
el.Label,
buttons,
formFieldErrors(el.Form, el.FormField),
)
}
func SelectList(el OptionsParams) Node {
buttons := make(Group, len(el.Options))
for i, opt := range el.Options {
buttons[i] = Option(
Text(opt.Label),
Value(opt.Value),
If(opt.Value == el.Value, Attr("selected")),
)
}
return Fieldset(
el.Label,
Select(
Class("select "+formFieldStatusClass(el.Form, el.FormField)),
Name(el.Name),
buttons,
),
Help(el.Help),
formFieldErrors(el.Form, el.FormField),
)
}
func Checkbox(el CheckboxParams) Node {
return Div(
Label(
Class("label"),
Input(
Class("checkbox"),
Type("checkbox"),
Name(el.Name),
If(el.Checked, Checked()),
Value("true"),
),
Text(" "+el.Label),
),
formFieldErrors(el.Form, el.FormField),
)
}
func InputField(el InputFieldParams) Node {
return Fieldset(
el.Label,
Input(
ID(el.Name),
Name(el.Name),
Type(el.InputType),
Class("input "+formFieldStatusClass(el.Form, el.FormField)),
Value(el.Value),
If(el.Placeholder != "", Placeholder(el.Placeholder)),
),
Help(el.Help),
formFieldErrors(el.Form, el.FormField),
)
}
func Help(text string) Node {
return If(len(text) > 0, Div(
Class("label"),
Text(text),
))
}
func Fieldset(label string, els ...Node) Node {
return FieldSet(
Class("fieldset"),
If(len(label) > 0, Legend(
Class("fieldset-legend"),
Text(label),
)),
Group(els),
)
}
func FileField(el FileFieldParams) Node {
return Fieldset(
el.Label,
Input(
Type("file"),
Class("file-input"),
Name(el.Name),
),
Help(el.Help),
)
}
func formFieldStatusClass(fm form.Form, formField string) string {
switch {
case fm == nil:
return ""
case !fm.IsSubmitted():
return ""
case fm.FieldHasErrors(formField):
return "input-error"
default:
return "input-success"
}
}
func formFieldErrors(fm form.Form, field string) Node {
if fm == nil {
return nil
}
errs := fm.GetFieldErrors(field)
if len(errs) == 0 {
return nil
}
g := make(Group, len(errs))
for i, err := range errs {
g[i] = Div(
Class("text-error"),
Text(err),
)
}
return g
}
func CSRF(r *ui.Request) Node {
return Input(
Type("hidden"),
Name("csrf"),
Value(r.CSRF),
)
}
func FormButton(color Color, label string) Node {
return Button(
Class("btn "+buttonColor(color)),
Text(label),
)
}
func ButtonLink(color Color, href, label string) Node {
return A(
Href(href),
Class("btn "+buttonColor(color)),
Text(label),
)
}
func buttonColor(color Color) string {
// Only colors being used are included so unused styles are not compiled.
switch color {
case ColorPrimary:
return "btn-primary"
case ColorInfo:
return "btn-info"
case ColorAccent:
return "btn-accent"
case ColorError:
return "btn-error"
case ColorLink:
return "btn-link"
default:
return ""
}
}

View file

@ -0,0 +1,35 @@
package components
import (
"strings"
"github.com/camzawacki/personal-site/internal/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func JS() Node {
return Group{
Script(Src("https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"), Defer()),
Script(Src("https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"), Defer()),
}
}
func CSS() Node {
return Link(
Href(ui.StaticFile("main.css")),
Rel("stylesheet"),
Type("text/css"),
)
}
func Metatags(r *ui.Request) Node {
return Group{
Meta(Charset("utf-8")),
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
Link(Rel("icon"), Href(ui.StaticFile("favicon.png"))),
TitleEl(Text(r.Config.App.Name), If(r.Title != "", Text(" | "+r.Title))),
If(r.Metatags.Description != "", Meta(Name("description"), Content(r.Metatags.Description))),
If(len(r.Metatags.Keywords) > 0, Meta(Name("keywords"), Content(strings.Join(r.Metatags.Keywords, ", ")))),
}
}

View file

@ -0,0 +1,39 @@
package components
import (
"fmt"
"github.com/camzawacki/personal-site/internal/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func HtmxListeners(r *ui.Request) Node {
const htmxErr = `
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.xhr.status >= 400){
evt.detail.shouldSwap = true;
evt.detail.target = htmx.find("body");
}
});
`
const htmxCSRF = `
document.body.addEventListener('htmx:configRequest', function(evt) {
if (evt.detail.verb !== "get") {
evt.detail.parameters['csrf'] = '%s';
}
})
`
return Group{
Script(Raw(htmxErr)),
Iff(len(r.CSRF) > 0, func() Node {
return Script(Raw(fmt.Sprintf(htmxCSRF, r.CSRF)))
}),
}
}
func HxBoost() Node {
return Attr("hx-boost", "true")
}

View file

@ -0,0 +1,91 @@
package components
import (
"fmt"
"github.com/camzawacki/personal-site/internal/pager"
"github.com/camzawacki/personal-site/internal/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/components"
. "maragu.dev/gomponents/html"
)
func NavLink(r *ui.Request, title, routeName string, disabled bool, routeParams ...any) Node {
href := r.Path(routeName, routeParams...)
var link Node
if disabled {
link = Span(
Class("text-xl text-base-content/40"),
Text(title),
)
} else {
link = A(
Class("text-xl hover:underline cursor-pointer"),
Href(href),
Text(title),
)
}
return link
}
func MenuLink(r *ui.Request, icon Node, title, routeName string, routeParams ...any) Node {
href := r.Path(routeName, routeParams...)
return Li(
Class("ml-2"),
A(
Href(href),
icon,
Text(title),
Classes{
"menu-active": href == r.CurrentPath,
"p-2": true,
},
),
)
}
func Pager(page int, path string, hasNext bool, hxTarget string) Node {
href := func(page int) string {
return fmt.Sprintf("%s?%s=%d",
path,
pager.QueryKey,
page,
)
}
return Div(
Class("join"),
A(
Class("join-item btn"),
Text("«"),
If(page <= 1, Disabled()),
Href(href(page-1)),
Iff(len(hxTarget) > 0, func() Node {
return Group{
Attr("hx-get", href(page-1)),
Attr("hx-swap", "outerHTML"),
Attr("hx-target", hxTarget),
}
}),
),
Button(
Class("join-item btn"),
Textf("Page %d", page),
),
A(
Class("join-item btn"),
Text("»"),
If(!hasNext, Disabled()),
Href(href(page+1)),
Iff(len(hxTarget) > 0, func() Node {
return Group{
Attr("hx-get", href(page+1)),
Attr("hx-swap", "outerHTML"),
Attr("hx-target", hxTarget),
}
}),
),
)
}

View file

@ -0,0 +1,27 @@
package components
type (
Color int
Size int
)
const (
ColorNone Color = iota
ColorNeutral
ColorPrimary
ColorSecondary
ColorAccent
ColorInfo
ColorSuccess
ColorWarning
ColorError
ColorLink
)
const (
SizeExtraSmall Size = iota
SizeSmall
SizeMedium
SizeLarge
SizeExtraLarge
)

View file

@ -0,0 +1,38 @@
package components
import (
"fmt"
"math/rand"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Tab struct {
Title, Body string
}
func Tabs(tabs []Tab) Node {
g := make(Group, 0, len(tabs)*2)
id := fmt.Sprintf("tabs-%d", rand.Int())
for i, tab := range tabs {
g = append(g,
Input(
Type("radio"),
Name(id),
Class("tab"),
Aria("label", tab.Title),
If(i == 0, Checked()),
),
Div(
Class("tab-content bg-base-100 border-base-300 p-6"),
Raw(tab.Body),
))
}
return Div(
Class("tabs tabs-lift"),
g,
)
}