This commit is contained in:
41666 2025-04-05 22:02:17 -07:00
parent 8c8cbfd7dd
commit 41d48bf60a
28 changed files with 434 additions and 7 deletions

0
auth/discordauth.go Normal file
View file

View file

@ -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"

View file

@ -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
rm -rf tmp result
open-chromium path="/testing/t/picker":
nix-shell -p chromium --command "chromium http://localhost:8170{{path}}"

58
presentation/role.go Normal file
View file

@ -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),
}
}

33
presentation/role_test.go Normal file
View file

@ -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)
}

View file

@ -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"

View file

@ -4,5 +4,7 @@
just
nil
air
nodePackages.prettier
pre-commit
];
}

4
static/images/check.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect fill="#000" width="2" height="5" x="3" y="4" style="transform: rotate(-45deg); transform-origin: center;" />
<rect fill="#000" width="2" height="7" x="7" y="4.5" style="transform: rotate(45deg); transform-origin: center;" />
</svg>

After

Width:  |  Height:  |  Size: 325 B

View file

@ -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);
}
}
}

View file

@ -0,0 +1,4 @@
<nav>
<div class="logo">Roleypoly</div>
<div class="username">41666</div>
</nav>

View file

@ -0,0 +1,10 @@
{{ $for := printf "category-%s_role-%s" .CategoryID .ID }}
<div
class="role"
style="--role-color: {{.Colors.Main}}; --contrast-color: var({{if .Colors.IsDark}}--grey600{{else}}--grey100{{end}});"
data-testid="role-{{.ID}}"
>
<input type="{{.InputType}}" id="{{$for}}" {{if eq .InputType
"radio"}}name="category_group_{{.CategoryID}}"{{end}} />
<label for="{{$for}}">{{.Name}}</label>
</div>

View file

@ -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)
}

View file

@ -11,6 +11,6 @@
<link rel="stylesheet" href="/static/main.css" />
</head>
<body>
{{embed}}
{{ template "components/nav" . }} {{embed}}
</body>
</html>

View file

@ -0,0 +1,16 @@
<style>
.container {
margin: 3rem;
}
</style>
<div class="container">
<div class="cat-multi">
{{ template "components/role" .TestRole }} {{ template "components/role"
.TestRole2 }}
</div>
<div class="cat-single">
{{ template "components/role" .TestRole3 }} {{ template "components/role"
.TestRole4 }}
</div>
</div>

View file

@ -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 {

15
types/category.go Normal file
View file

@ -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
}

View file

@ -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
}

32
types/fixtures/role.go Normal file
View file

@ -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)
)

11
types/role.go Normal file
View file

@ -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"`
}

21
utils/colors.go Normal file
View file

@ -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)
}

29
utils/colors_test.go Normal file
View file

@ -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)
}