From 41d48bf60acf0d7a66ad60db4373bef1846e5457 Mon Sep 17 00:00:00 2001 From: noe Date: Sat, 5 Apr 2025 22:02:17 -0700 Subject: [PATCH] roles --- .../authmiddleware}/authmiddleware.go | 0 .../authmiddleware}/const.go | 0 .../authmiddleware}/must.go | 0 .../authmiddleware}/must_test.go | 0 .../authmiddleware}/session.go | 0 .../authmiddleware}/util_test.go | 0 .../authmiddleware}/validation_test.go | 0 auth/discordauth.go | 0 interactions/interactions_test.go | 2 +- justfile | 10 +- presentation/role.go | 58 ++++++++++++ presentation/role_test.go | 33 +++++++ roleypoly/fiber.go | 2 +- shell.nix | 2 + static/images/check.svg | 4 + static/main.css | 91 +++++++++++++++++++ templates/components/nav.html | 4 + templates/components/role.html | 10 ++ templates/components/role_test.go | 57 ++++++++++++ templates/layouts/main.html | 2 +- templates/tests/picker.html | 16 ++++ testing/testing.go | 13 ++- types/category.go | 15 +++ types/fixtures/category.go | 29 ++++++ types/fixtures/role.go | 32 +++++++ types/role.go | 11 +++ utils/colors.go | 21 +++++ utils/colors_test.go | 29 ++++++ 28 files changed, 434 insertions(+), 7 deletions(-) rename {authmiddleware => auth/authmiddleware}/authmiddleware.go (100%) rename {authmiddleware => auth/authmiddleware}/const.go (100%) rename {authmiddleware => auth/authmiddleware}/must.go (100%) rename {authmiddleware => auth/authmiddleware}/must_test.go (100%) rename {authmiddleware => auth/authmiddleware}/session.go (100%) rename {authmiddleware => auth/authmiddleware}/util_test.go (100%) rename {authmiddleware => auth/authmiddleware}/validation_test.go (100%) create mode 100644 auth/discordauth.go create mode 100644 presentation/role.go create mode 100644 presentation/role_test.go create mode 100644 static/images/check.svg create mode 100644 templates/components/nav.html create mode 100644 templates/components/role.html create mode 100644 templates/components/role_test.go create mode 100644 templates/tests/picker.html create mode 100644 types/category.go create mode 100644 types/fixtures/category.go create mode 100644 types/fixtures/role.go create mode 100644 types/role.go create mode 100644 utils/colors.go create mode 100644 utils/colors_test.go diff --git a/authmiddleware/authmiddleware.go b/auth/authmiddleware/authmiddleware.go similarity index 100% rename from authmiddleware/authmiddleware.go rename to auth/authmiddleware/authmiddleware.go diff --git a/authmiddleware/const.go b/auth/authmiddleware/const.go similarity index 100% rename from authmiddleware/const.go rename to auth/authmiddleware/const.go diff --git a/authmiddleware/must.go b/auth/authmiddleware/must.go similarity index 100% rename from authmiddleware/must.go rename to auth/authmiddleware/must.go diff --git a/authmiddleware/must_test.go b/auth/authmiddleware/must_test.go similarity index 100% rename from authmiddleware/must_test.go rename to auth/authmiddleware/must_test.go diff --git a/authmiddleware/session.go b/auth/authmiddleware/session.go similarity index 100% rename from authmiddleware/session.go rename to auth/authmiddleware/session.go diff --git a/authmiddleware/util_test.go b/auth/authmiddleware/util_test.go similarity index 100% rename from authmiddleware/util_test.go rename to auth/authmiddleware/util_test.go diff --git a/authmiddleware/validation_test.go b/auth/authmiddleware/validation_test.go similarity index 100% rename from authmiddleware/validation_test.go rename to auth/authmiddleware/validation_test.go diff --git a/auth/discordauth.go b/auth/discordauth.go new file mode 100644 index 0000000..e69de29 diff --git a/interactions/interactions_test.go b/interactions/interactions_test.go index 1abfcb6..0aac146 100644 --- a/interactions/interactions_test.go +++ b/interactions/interactions_test.go @@ -7,7 +7,7 @@ import ( "net/http" "testing" - "git.sapphic.engineer/roleypoly/v4/authmiddleware" + "git.sapphic.engineer/roleypoly/v4/auth/authmiddleware" "git.sapphic.engineer/roleypoly/v4/discord" "git.sapphic.engineer/roleypoly/v4/interactions" "git.sapphic.engineer/roleypoly/v4/roleypoly" diff --git a/justfile b/justfile index 6e7217e..268eea8 100644 --- a/justfile +++ b/justfile @@ -12,10 +12,13 @@ run-container: docker load -i result docker run -it --rm -p 8169:8169 localhost/roleypoly/roleypoly -precommit: fmt tidy update-vendor-hash test +precommit: fmt prettier tidy update-vendor-hash test fmt: go fmt ./... + +prettier: + prettier -w -c **/*.{css,html,json,md} tidy: go mod tidy @@ -27,4 +30,7 @@ test: go test ./... clean-repo: - rm -rf tmp result \ No newline at end of file + rm -rf tmp result + +open-chromium path="/testing/t/picker": + nix-shell -p chromium --command "chromium http://localhost:8170{{path}}" \ No newline at end of file diff --git a/presentation/role.go b/presentation/role.go new file mode 100644 index 0000000..c320b99 --- /dev/null +++ b/presentation/role.go @@ -0,0 +1,58 @@ +package presentation + +import ( + "fmt" + + "git.sapphic.engineer/roleypoly/v4/types" + "git.sapphic.engineer/roleypoly/v4/utils" +) + +type InputType string + +const ( + InputCheckbox InputType = "checkbox" + InputRadio InputType = "radio" +) + +type PresentableRole struct { + ID string + CategoryID string + Name string + Selected bool + InputType InputType + Colors PresentableRoleColors +} + +func Role(category *types.Category, role *types.Role, selected bool) PresentableRole { + inputType := InputCheckbox + if category.Type == types.CategorySingle { + inputType = InputRadio + } + + colors := GetColors(role.Color) + + return PresentableRole{ + ID: role.ID, + CategoryID: category.ID, + Name: role.Name, + Selected: selected, + InputType: inputType, + Colors: colors, + } +} + +type PresentableRoleColors struct { + Main string + IsDark bool +} + +func GetColors(roleColor uint32) PresentableRoleColors { + // TODO: no color + + r, g, b := utils.IntToRgb(roleColor) + + return PresentableRoleColors{ + Main: fmt.Sprintf("#%x", roleColor), + IsDark: utils.IsDarkColor(r, g, b), + } +} diff --git a/presentation/role_test.go b/presentation/role_test.go new file mode 100644 index 0000000..fdc1c14 --- /dev/null +++ b/presentation/role_test.go @@ -0,0 +1,33 @@ +package presentation_test + +import ( + "testing" + + "git.sapphic.engineer/roleypoly/v4/presentation" + "git.sapphic.engineer/roleypoly/v4/types/fixtures" + "github.com/stretchr/testify/assert" +) + +func TestRole(t *testing.T) { + r := presentation.Role(&fixtures.CategoryMulti, &fixtures.RoleWithDarkColor, true) + assert.Equal(t, fixtures.RoleWithDarkColor.ID, r.ID) + assert.Equal(t, fixtures.RoleWithDarkColor.Name, r.Name) + assert.Equal(t, presentation.InputCheckbox, r.InputType) + assert.Equal(t, "#a20000", r.Colors.Main) + assert.True(t, r.Colors.IsDark) + assert.True(t, r.Selected) + + r = presentation.Role(&fixtures.CategorySingle, &fixtures.RoleWithDarkColor, false) + assert.Equal(t, presentation.InputRadio, r.InputType) + assert.False(t, r.Selected) + + r = presentation.Role(&fixtures.CategorySingle, &fixtures.RoleWithLightColor, true) + assert.False(t, r.Colors.IsDark) + assert.True(t, r.Selected) +} + +func TestGetColors(t *testing.T) { + c := presentation.GetColors(0xa20000) + assert.Equal(t, "#a20000", c.Main) + assert.True(t, c.IsDark) +} diff --git a/roleypoly/fiber.go b/roleypoly/fiber.go index d546d41..669ee6e 100644 --- a/roleypoly/fiber.go +++ b/roleypoly/fiber.go @@ -13,7 +13,7 @@ import ( "github.com/gofiber/fiber/v3/middleware/static" "github.com/gofiber/template/html/v2" - "git.sapphic.engineer/roleypoly/v4/authmiddleware" + "git.sapphic.engineer/roleypoly/v4/auth/authmiddleware" "git.sapphic.engineer/roleypoly/v4/discord" "git.sapphic.engineer/roleypoly/v4/interactions" staticfs "git.sapphic.engineer/roleypoly/v4/static" diff --git a/shell.nix b/shell.nix index 944bef8..49c0e43 100644 --- a/shell.nix +++ b/shell.nix @@ -4,5 +4,7 @@ just nil air + nodePackages.prettier + pre-commit ]; } diff --git a/static/images/check.svg b/static/images/check.svg new file mode 100644 index 0000000..7c56d2f --- /dev/null +++ b/static/images/check.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/static/main.css b/static/main.css index c0c10b1..409d3de 100644 --- a/static/main.css +++ b/static/main.css @@ -1,3 +1,94 @@ +:root { + --taupe100: #332d2d; + --taupe200: #453e3d; + --taupe300: #5d5352; + --taupe400: #756867; + --taupe500: #ab9b9a; + --taupe600: #ebd6d4; + + --discord100: #23272a; + --discord200: #2c2f33; + --discord400: #7289da; + --discord500: #99aab5; + + --green400: #46b646; + --green200: #1d8227; + + --red400: #e95353; + --red200: #f14343; + + --gold400: #efcf24; + + --grey100: #1c1010; + --grey500: #dbd9d9; + --grey600: #f2efef; +} + +* { + box-sizing: border-box; +} + body { font-family: "Atkinson Hyperlegible", sans-serif; + background-color: var(--taupe200); + color: var(--taupe600); +} + +.role { + /* TODO: dont do this, don't remember why?? (^aki) */ + display: inline-flex; + align-items: center; + border: 2px solid var(--role-color); + border-radius: 3px; + user-select: none; + padding: 0.217rem; /* this is silly number pls ignore :3 (^noe) */ + gap: 0.215rem; + + input { + appearance: none; + position: relative; + width: 1.216rem; + height: 1.216rem; + margin: 0; + padding: 0; + border: 2px solid var(--role-color); + transition: all 0.35s ease-in-out; + border-radius: 1.216rem; + + &::before { + transition: all 0.35s ease-in-out; + content: ""; + mask-image: url(/static/images/check.svg); + mask-size: cover; + mask-position: center center; + background-color: var(--role-color); + opacity: 0; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 1.216rem; + } + } + + label { + } + + &:has(input:checked) { + background-color: var(--role-color); + + input { + border-color: var(--contrast-color); + opacity: 1; + + &::before { + opacity: 1; + background-color: var(--contrast-color); + } + } + + label { + color: var(--contrast-color); + } + } } diff --git a/templates/components/nav.html b/templates/components/nav.html new file mode 100644 index 0000000..d44dbac --- /dev/null +++ b/templates/components/nav.html @@ -0,0 +1,4 @@ + diff --git a/templates/components/role.html b/templates/components/role.html new file mode 100644 index 0000000..40f0963 --- /dev/null +++ b/templates/components/role.html @@ -0,0 +1,10 @@ +{{ $for := printf "category-%s_role-%s" .CategoryID .ID }} +
+ + +
diff --git a/templates/components/role_test.go b/templates/components/role_test.go new file mode 100644 index 0000000..e4705c0 --- /dev/null +++ b/templates/components/role_test.go @@ -0,0 +1,57 @@ +package components_test + +import ( + "bytes" + "fmt" + "html/template" + "testing" + + "git.sapphic.engineer/roleypoly/v4/presentation" + "git.sapphic.engineer/roleypoly/v4/templates" + "git.sapphic.engineer/roleypoly/v4/types" + "git.sapphic.engineer/roleypoly/v4/types/fixtures" + "github.com/stretchr/testify/assert" +) + +var ( + Templates = template.Must(template.ParseFS(templates.FS, "components/*.html")) +) + +func renderRole(t *testing.T, c *types.Category, r *types.Role, s bool) string { + data := presentation.Role(c, r, s) + buf := bytes.Buffer{} + err := Templates.ExecuteTemplate(&buf, "role.html", data) + if err != nil { + t.Fatal("template failed to render", err) + } + return buf.String() +} + +func TestRole(t *testing.T) { + c := &fixtures.CategoryMulti + r := &fixtures.RoleWithDarkColor + html := renderRole(t, c, r, false) + assert.Contains(t, html, "--role-color: #a20000;", "role color is set") + assert.Contains(t, html, "--contrast-color: var(--grey600);", "contrast color is set") + assert.Contains(t, html, fmt.Sprintf(`id="%s"`, roleInputID(c, r)), "input has ID attr") + assert.Contains(t, html, fmt.Sprintf(`for="%s"`, roleInputID(c, r)), "label has for attr") + assert.Contains(t, html, `type="checkbox"`, "multi has input type=checkbox") + assert.NotContains(t, html, fmt.Sprintf(`name="%s"`, roleInputName(c)), "multi has no name attr") + // TODO: selected? + + c = &fixtures.CategorySingle + r = &fixtures.RoleWithLightColor + html = renderRole(t, c, r, true) + assert.Contains(t, html, "--contrast-color: var(--grey100);", "single has name attr") + assert.Contains(t, html, `type="radio"`, "single has input type=radio") + assert.Contains(t, html, fmt.Sprintf(`name="%s"`, roleInputName(c)), "single has name attr") +} + +// TODO: these can probably be string utils that are injected as functions into template +func roleInputID(c *types.Category, r *types.Role) string { + return fmt.Sprintf("category-%s_role-%s", c.ID, r.ID) +} + +func roleInputName(c *types.Category) string { + return fmt.Sprintf("category_group_%s", c.ID) +} diff --git a/templates/layouts/main.html b/templates/layouts/main.html index 679194f..06f98da 100644 --- a/templates/layouts/main.html +++ b/templates/layouts/main.html @@ -11,6 +11,6 @@ - {{embed}} + {{ template "components/nav" . }} {{embed}} diff --git a/templates/tests/picker.html b/templates/tests/picker.html new file mode 100644 index 0000000..01599fb --- /dev/null +++ b/templates/tests/picker.html @@ -0,0 +1,16 @@ + + +
+
+ {{ template "components/role" .TestRole }} {{ template "components/role" + .TestRole2 }} +
+
+ {{ template "components/role" .TestRole3 }} {{ template "components/role" + .TestRole4 }} +
+
diff --git a/testing/testing.go b/testing/testing.go index 3607e4b..dcd59f2 100644 --- a/testing/testing.go +++ b/testing/testing.go @@ -5,9 +5,11 @@ import ( "github.com/gofiber/fiber/v3" - "git.sapphic.engineer/roleypoly/v4/authmiddleware" + "git.sapphic.engineer/roleypoly/v4/auth/authmiddleware" "git.sapphic.engineer/roleypoly/v4/discord" + "git.sapphic.engineer/roleypoly/v4/presentation" "git.sapphic.engineer/roleypoly/v4/types" + "git.sapphic.engineer/roleypoly/v4/types/fixtures" ) type TestingController struct { @@ -34,7 +36,14 @@ func (t *TestingController) Picker(c fiber.Ctx) error { func (t *TestingController) TestTemplate(c fiber.Ctx) error { which := c.Params("which") - return c.Render("tests/"+which, fiber.Map{}) + cat1 := fixtures.Category(fixtures.CategoryMulti) + cat2 := fixtures.Category(fixtures.CategorySingle) + return c.Render("tests/"+which, fiber.Map{ + "TestRole": presentation.Role(cat1, &fixtures.RoleWithDarkColor, false), + "TestRole2": presentation.Role(cat1, &fixtures.RoleWithLightColor, true), + "TestRole3": presentation.Role(cat2, &fixtures.RoleWithDarkColor, false), + "TestRole4": presentation.Role(cat2, &fixtures.RoleWithLightColor, true), + }, "layouts/main") } func (t *TestingController) GetMember(c fiber.Ctx) error { diff --git a/types/category.go b/types/category.go new file mode 100644 index 0000000..251bfb2 --- /dev/null +++ b/types/category.go @@ -0,0 +1,15 @@ +package types + +type CategoryType string + +const ( + CategoryMultiple CategoryType = "multiple" + CategorySingle CategoryType = "single" +) + +type Category struct { + ID string + Name string + Type CategoryType + Roles []string // of role IDs +} diff --git a/types/fixtures/category.go b/types/fixtures/category.go new file mode 100644 index 0000000..02db33f --- /dev/null +++ b/types/fixtures/category.go @@ -0,0 +1,29 @@ +package fixtures + +import ( + "fmt" + "math/rand" + + "git.sapphic.engineer/roleypoly/v4/types" +) + +var ( + CategoryMulti = types.Category{ + ID: "multi", + Name: "Roles", + Type: types.CategoryMultiple, + Roles: []string{RoleWithDarkColor.ID, RoleWithLightColor.ID, RoleWithoutColor.ID}, + } + + CategorySingle = types.Category{ + ID: "single", + Name: "Roles", + Type: types.CategorySingle, + Roles: []string{RoleWithDarkColor.ID, RoleWithLightColor.ID, RoleWithoutColor.ID}, + } +) + +func Category(base types.Category) *types.Category { + base.ID = fmt.Sprintf("%s-%d", base.ID, rand.Uint32()) + return &base +} diff --git a/types/fixtures/role.go b/types/fixtures/role.go new file mode 100644 index 0000000..9abbe30 --- /dev/null +++ b/types/fixtures/role.go @@ -0,0 +1,32 @@ +package fixtures + +import "git.sapphic.engineer/roleypoly/v4/types" + +var ( + RoleWithDarkColor = types.Role{ + ID: "dark-color", + Name: "role with dark color", + Color: 0xa20000, + Permissions: 0, + Position: 10, + } + RoleWithLightColor = types.Role{ + ID: "light-color", + Name: "role with light color", + Color: 0xffaa88, + Permissions: 0, + Position: 10, + } + RoleWithoutColor = types.Role{ + ID: "without-color", + Name: "role", + Color: 0x000000, + Permissions: 0, + Position: 11, + } + //TODO: role with admin (bad) + //TODO: role with manage roles (bad) + //TODO: role above roleypoly (bad) + //TODO: role with managed (bad) + //TODO: role that is roleypoly (hi) +) diff --git a/types/role.go b/types/role.go new file mode 100644 index 0000000..761790b --- /dev/null +++ b/types/role.go @@ -0,0 +1,11 @@ +package types + +type Role struct { + ID string `json:"id"` + Name string `json:"name"` + Color uint32 `json:"color"` + Icon string `json:"icon"` // unused, future? + UnicodeEmoji string `json:"unicode_emoji"` // unused, future? + Permissions uint64 `json:"permissions,string"` + Position uint8 `json:"position"` +} diff --git a/utils/colors.go b/utils/colors.go new file mode 100644 index 0000000..b5c16dc --- /dev/null +++ b/utils/colors.go @@ -0,0 +1,21 @@ +package utils + +import "math" + +func IsDarkColor(r, g, b uint8) bool { + rC := 0.299 * math.Pow(float64(r), 2) + gC := 0.587 * math.Pow(float64(g), 2) + bC := 0.114 * math.Pow(float64(b), 2) + + lum := math.Sqrt(rC + gC + bC) + + return lum <= 130 +} + +func IntToRgb(color uint32) (uint8, uint8, uint8) { + b := color % 256 + g := color / 256 % 256 + r := color / (256 * 256) % 256 + + return uint8(r), uint8(g), uint8(b) +} diff --git a/utils/colors_test.go b/utils/colors_test.go new file mode 100644 index 0000000..2aea6ec --- /dev/null +++ b/utils/colors_test.go @@ -0,0 +1,29 @@ +package utils_test + +import ( + "testing" + + "git.sapphic.engineer/roleypoly/v4/utils" + "github.com/stretchr/testify/assert" +) + +func TestIntToRgb(t *testing.T) { + r, g, b := utils.IntToRgb(0x123456) + assert.Equal(t, uint8(0x12), r, "red") + assert.Equal(t, uint8(0x34), g, "green") + assert.Equal(t, uint8(0x56), b, "blue") +} + +func TestIsDarkColor(t *testing.T) { + isDark := utils.IsDarkColor(0, 0, 0) + assert.True(t, isDark) + + isLight := utils.IsDarkColor(255, 255, 255) + assert.False(t, isLight) + + isQuestionable := utils.IsDarkColor(0x88, 0x88, 0x88) + assert.False(t, isQuestionable) + + isReallyQuestionable := utils.IsDarkColor(0x00, 0x88, 0x00) + assert.True(t, isReallyQuestionable) +}