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,124 @@
package forms
import (
"net/http"
"net/url"
"entgo.io/ent/schema/field"
"github.com/camzawacki/personal-site/ent/admin"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func AdminEntity(r *ui.Request, entityType admin.EntityType, values url.Values) Node {
// TODO inline validation?
isNew := values == nil
nodes := make(Group, 0)
getValue := func(name string) string {
// Values in the submitted form take precedence.
if value := r.Context.FormValue(name); value != "" {
return value
}
// Fallback to the entity's values, if being edited.
if values != nil && len(values[name]) > 0 {
return values[name][0]
}
return ""
}
// Attempt to add form elements for all editable entity fields.
for _, f := range entityType.GetSchema() {
// TODO cardinality?
if !isNew && f.Immutable {
continue
}
switch f.Type {
case field.TypeString:
p := InputFieldParams{
Name: f.Name,
InputType: "text",
Label: admin.FieldLabel(f.Name),
Value: getValue(f.Name),
}
if f.Sensitive {
p.InputType = "password"
if !isNew {
p.Placeholder = "*****"
p.Help = "SENSITIVE: This field will only be updated if a value is provided."
}
}
nodes = append(nodes, InputField(p))
case field.TypeTime:
nodes = append(nodes, InputField(InputFieldParams{
Name: f.Name,
InputType: "datetime-local",
Label: admin.FieldLabel(f.Name),
Value: getValue(f.Name),
}))
case field.TypeInt, field.TypeInt8, field.TypeInt16, field.TypeInt32, field.TypeInt64,
field.TypeUint, field.TypeUint8, field.TypeUint16, field.TypeUint32, field.TypeUint64,
field.TypeFloat32, field.TypeFloat64:
nodes = append(nodes, InputField(InputFieldParams{
Name: f.Name,
InputType: "number",
Label: admin.FieldLabel(f.Name),
Value: getValue(f.Name),
}))
case field.TypeBool:
nodes = append(nodes, Checkbox(CheckboxParams{
Name: f.Name,
Label: admin.FieldLabel(f.Name),
Checked: getValue(f.Name) == "true",
}))
case field.TypeEnum:
options := make([]Choice, 0, len(f.Enums)+1)
if f.Optional {
options = append(options, Choice{
Label: "-",
Value: "",
})
}
for _, enum := range f.Enums {
options = append(options, Choice{
Label: enum,
Value: enum,
})
}
nodes = append(nodes, SelectList(OptionsParams{
Name: f.Name,
Label: admin.FieldLabel(f.Name),
Value: getValue(f.Name),
Options: options,
}))
default:
nodes = append(nodes, P(Textf("%s not supported", f.Name)))
}
}
return Form(
Method(http.MethodPost),
nodes,
ControlGroup(
FormButton(ColorPrimary, "Submit"),
ButtonLink(
ColorNone,
r.Path(routenames.AdminEntityList(entityType.GetName())),
"Cancel",
),
),
CSRF(r),
)
}

View file

@ -0,0 +1,30 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/ent/admin"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func AdminEntityDelete(r *ui.Request, entityType admin.EntityType) Node {
return Form(
Method(http.MethodPost),
P(
Textf("Are you sure you want to delete this %s?", entityType.GetName()),
),
ControlGroup(
FormButton(ColorError, "Delete"),
ButtonLink(
ColorNone,
r.Path(routenames.AdminEntityList(entityType.GetName())),
"Cancel",
),
),
CSRF(r),
)
}

View file

@ -0,0 +1,54 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Cache struct {
CurrentValue string
Value string `form:"value"`
form.Submission
}
func (f *Cache) Render(r *ui.Request) Node {
return Form(
ID("cache"),
Method(http.MethodPost),
Attr("hx-post", r.Path(routenames.CacheSubmit)),
Card(CardParams{
Title: "Test the cache",
Body: Group{
Span(Text("This route handler shows how the default in-memory cache works. Try updating the value using the form below and see how it persists after you reload the page.")),
Span(Text("HTMX makes it easy to re-render the cached value after the form is submitted.")),
},
Color: ColorInfo,
Size: SizeMedium,
}),
Label(
For("value"),
Class("value"),
Text("Value in cache: "),
),
If(f.CurrentValue != "", Badge(ColorSuccess, f.CurrentValue)),
If(f.CurrentValue == "", Badge(ColorWarning, "empty")),
InputField(InputFieldParams{
Form: f,
FormField: "Value",
Name: "value",
InputType: "text",
Label: "Value",
Value: f.Value,
}),
ControlGroup(
FormButton(ColorPrimary, "Update cache"),
),
CSRF(r),
)
}

View file

@ -0,0 +1,58 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Contact struct {
Email string `form:"email" validate:"required,email"`
Department string `form:"department" validate:"required,oneof=sales marketing hr"`
Message string `form:"message" validate:"required"`
form.Submission
}
func (f *Contact) Render(r *ui.Request) Node {
return Form(
ID("contact"),
Method(http.MethodPost),
Attr("hx-post", r.Path(routenames.ContactSubmit)),
InputField(InputFieldParams{
Form: f,
FormField: "Email",
Name: "email",
InputType: "email",
Label: "Email address",
Value: f.Email,
}),
Radios(OptionsParams{
Form: f,
FormField: "Department",
Name: "department",
Label: "Department",
Value: f.Department,
Options: []Choice{
{Value: "sales", Label: "Sales"},
{Value: "marketing", Label: "Marketing"},
{Value: "hr", Label: "HR"},
},
}),
TextareaField(TextareaFieldParams{
Form: f,
FormField: "Message",
Name: "message",
Label: "Message",
Value: f.Message,
}),
ControlGroup(
FormButton(ColorPrimary, "Submit"),
),
CSRF(r),
)
}

31
internal/ui/forms/file.go Normal file
View file

@ -0,0 +1,31 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type File struct{}
func (f File) Render(r *ui.Request) Node {
return Form(
ID("files"),
Method(http.MethodPost),
Action(r.Path(routenames.FilesSubmit)),
EncType("multipart/form-data"),
FileField(FileFieldParams{
Name: "file",
Label: "Test file",
Help: "Pick a file to upload.",
}),
ControlGroup(
FormButton(ColorPrimary, "Upload"),
),
CSRF(r),
)
}

View file

@ -0,0 +1,39 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type ForgotPassword struct {
Email string `form:"email" validate:"required,email"`
form.Submission
}
func (f *ForgotPassword) Render(r *ui.Request) Node {
return Form(
ID("forgot-password"),
Method(http.MethodPost),
HxBoost(),
Action(r.Path(routenames.ForgotPasswordSubmit)),
InputField(InputFieldParams{
Form: f,
FormField: "Email",
Name: "email",
InputType: "email",
Label: "Email address",
Value: f.Email,
}),
ControlGroup(
FormButton(ColorPrimary, "Reset password"),
ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
),
CSRF(r),
)
}

View file

@ -0,0 +1,64 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Login struct {
Email string `form:"email" validate:"required,email"`
Password string `form:"password" validate:"required"`
form.Submission
}
func (f *Login) Render(r *ui.Request) Node {
return Form(
ID("login"),
Method(http.MethodPost),
HxBoost(),
Action(r.Path(routenames.LoginSubmit)),
FlashMessages(r),
InputField(InputFieldParams{
Form: f,
FormField: "Email",
Name: "email",
InputType: "email",
Label: "Email address",
Value: f.Email,
}),
InputField(InputFieldParams{
Form: f,
FormField: "Password",
Name: "password",
InputType: "password",
Label: "Password",
Placeholder: "******",
}),
Div(
Class("text-right text-primary mt-2"),
A(
Href(r.Path(routenames.ForgotPassword)),
Text("Forgot password?"),
),
),
ControlGroup(
FormButton(ColorPrimary, "Login"),
ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
),
CSRF(r),
Div(
Class("text-center text-base-content/50 mt-4"),
Text("Don't have an account? "),
A(
Href(r.Path(routenames.Register)),
Text("Register"),
),
),
)
}

View file

@ -0,0 +1,74 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Register struct {
Name string `form:"name" validate:"required"`
Email string `form:"email" validate:"required,email"`
Password string `form:"password" validate:"required"`
ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
form.Submission
}
func (f *Register) Render(r *ui.Request) Node {
return Form(
ID("register"),
Method(http.MethodPost),
HxBoost(),
Action(r.Path(routenames.RegisterSubmit)),
InputField(InputFieldParams{
Form: f,
FormField: "Name",
Name: "name",
InputType: "text",
Label: "Name",
Value: f.Name,
}),
InputField(InputFieldParams{
Form: f,
FormField: "Email",
Name: "email",
InputType: "email",
Label: "Email address",
Value: f.Email,
}),
InputField(InputFieldParams{
Form: f,
FormField: "Password",
Name: "password",
InputType: "password",
Label: "Password",
Placeholder: "******",
}),
InputField(InputFieldParams{
Form: f,
FormField: "ConfirmPassword",
Name: "password-confirm",
InputType: "password",
Label: "Confirm password",
Placeholder: "******",
}),
ControlGroup(
FormButton(ColorPrimary, "Register"),
ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
),
CSRF(r),
Div(
Class("text-center text-base-content/50 mt-4"),
Text("Already have an account? "),
A(
Href(r.Path(routenames.Login)),
Text("Login"),
),
),
)
}

View file

@ -0,0 +1,46 @@
package forms
import (
"net/http"
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type ResetPassword struct {
Password string `form:"password" validate:"required"`
ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
form.Submission
}
func (f *ResetPassword) Render(r *ui.Request) Node {
return Form(
ID("reset-password"),
Method(http.MethodPost),
HxBoost(),
Action(r.CurrentPath),
InputField(InputFieldParams{
Form: f,
FormField: "Password",
Name: "password",
InputType: "password",
Label: "Password",
Placeholder: "******",
}),
InputField(InputFieldParams{
Form: f,
FormField: "PasswordConfirm",
Name: "password-confirm",
InputType: "password",
Label: "Confirm password",
Placeholder: "******",
}),
ControlGroup(
FormButton(ColorPrimary, "Update password"),
),
CSRF(r),
)
}

49
internal/ui/forms/task.go Normal file
View file

@ -0,0 +1,49 @@
package forms
import (
"fmt"
"net/http"
"github.com/camzawacki/personal-site/internal/form"
"github.com/camzawacki/personal-site/internal/routenames"
"github.com/camzawacki/personal-site/internal/ui"
. "github.com/camzawacki/personal-site/internal/ui/components"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Task struct {
Delay int `form:"delay" validate:"gte=0"`
Message string `form:"message" validate:"required"`
form.Submission
}
func (f *Task) Render(r *ui.Request) Node {
return Form(
ID("task"),
Method(http.MethodPost),
Attr("hx-post", r.Path(routenames.TaskSubmit)),
FlashMessages(r),
InputField(InputFieldParams{
Form: f,
FormField: "Delay",
Name: "delay",
InputType: "number",
Label: "Delay (in seconds)",
Help: "How long to wait until the task is executed",
Value: fmt.Sprint(f.Delay),
}),
TextareaField(TextareaFieldParams{
Form: f,
FormField: "Message",
Name: "message",
Label: "Message",
Value: f.Message,
Help: "The message the task will output to the log",
}),
ControlGroup(
FormButton(ColorPrimary, "Add task to queue"),
),
CSRF(r),
)
}