Added a basic homepage
This commit is contained in:
parent
d40640a648
commit
12fd3c04ca
113 changed files with 414 additions and 506 deletions
66
internal/ui/components/alerts.go
Normal file
66
internal/ui/components/alerts.go
Normal 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)),
|
||||
)
|
||||
}
|
||||
121
internal/ui/components/data.go
Normal file
121
internal/ui/components/data.go
Normal 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,
|
||||
)
|
||||
}
|
||||
268
internal/ui/components/form.go
Normal file
268
internal/ui/components/form.go
Normal 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 ""
|
||||
}
|
||||
}
|
||||
35
internal/ui/components/head.go
Normal file
35
internal/ui/components/head.go
Normal 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, ", ")))),
|
||||
}
|
||||
}
|
||||
39
internal/ui/components/htmx.go
Normal file
39
internal/ui/components/htmx.go
Normal 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")
|
||||
}
|
||||
91
internal/ui/components/nav.go
Normal file
91
internal/ui/components/nav.go
Normal 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),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
27
internal/ui/components/styles.go
Normal file
27
internal/ui/components/styles.go
Normal 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
|
||||
)
|
||||
38
internal/ui/components/tabs.go
Normal file
38
internal/ui/components/tabs.go
Normal 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,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue