aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--Makefile33
-rw-r--r--config/config.go205
-rw-r--r--config/constants.go7
-rw-r--r--config/types.go12
-rw-r--r--controllers/home.go12
-rw-r--r--eda/main.go52
-rw-r--r--go.mod26
-rw-r--r--go.sum51
-rw-r--r--processors/meta.go10
-rw-r--r--processors/processors.go7
-rw-r--r--router/routes.go12
-rw-r--r--templates/home.django4
-rw-r--r--templates/layouts/main.django30
-rw-r--r--utils/shortcuts/render.go71
15 files changed, 536 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index aaadf73..c1e39e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,7 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/
+
+# OS Specific
+.DS_Store
+Thumbs.db
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..fb20d96
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,33 @@
+BINARY_NAME=eda
+BUILD_PATH=bin/$(BINARY_NAME)
+MAIN_PATH=$(BINARY_NAME)/main.go
+
+.PHONY: setup clean build run dev all
+
+setup:
+ @echo "Setting up environment..."
+ @go mod download
+ @echo "Environment setup complete."
+
+clean:
+ @echo "Cleaning up..."
+ @rm -rf bin
+ @echo "Cleanup complete."
+
+build:
+ @echo "Building..."
+ @go build -o $(BUILD_PATH) $(MAIN_PATH) || true
+ @echo "Build complete."
+
+run:
+ @if [ ! -f $(BUILD_PATH) ]; then echo "Binary not found. Building binary..."; $(MAKE) -s build; fi
+ @echo "Running..."
+ @$(BUILD_PATH) || true
+
+dev:
+ @echo "Running in development mode..."
+ @go run $(MAIN_PATH) || true
+
+all: setup clean build run
+
+.SILENT: \ No newline at end of file
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..4ad3c25
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,205 @@
+package config
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "reflect"
+ "strconv"
+ "time"
+
+ "github.com/joho/godotenv"
+)
+
+var (
+ Server ServerConfig
+ GitHub GitHubConfig
+)
+
+func init() {
+ godotenv.Load()
+
+ if err := parse(&Server); err != nil {
+ log.Fatalf("failed to parse server config: %v", err)
+ }
+
+ if err := parse(&GitHub); err != nil {
+ log.Fatalf("failed to parse GitHub config: %v", err)
+ }
+}
+
+func getEnv(key, defaultVal string) string {
+ if value := os.Getenv(key); value != "" {
+ return value
+ }
+ return defaultVal
+}
+
+func getEnvBool(key string, defaultVal bool) bool {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := strconv.ParseBool(value); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func getEnvDuration(key string, defaultVal time.Duration) time.Duration {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := time.ParseDuration(value); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func getEnvInt64(key string, defaultVal int64) int64 {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func getEnvFloat64(key string, defaultVal float64) float64 {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := strconv.ParseFloat(value, 64); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func setFieldFromEnv(field reflect.Value, envKey, defaultVal string) {
+ switch field.Kind() {
+ case reflect.String:
+ field.SetString(getEnv(envKey, defaultVal))
+ case reflect.Bool:
+ defaultBool, _ := strconv.ParseBool(defaultVal)
+ field.SetBool(getEnvBool(envKey, defaultBool))
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ defaultInt, _ := strconv.ParseInt(defaultVal, 10, 64)
+ field.SetInt(getEnvInt64(envKey, defaultInt))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ defaultUint, _ := strconv.ParseUint(defaultVal, 10, 64)
+ setUintField(field, envKey, defaultUint)
+ case reflect.Float32, reflect.Float64:
+ defaultFloat, _ := strconv.ParseFloat(defaultVal, 64)
+ field.SetFloat(getEnvFloat64(envKey, defaultFloat))
+ default:
+ setDurationField(field, envKey, defaultVal)
+ }
+}
+
+func setUintField(field reflect.Value, envKey string, defaultVal uint64) {
+ if value := os.Getenv(envKey); value != "" {
+ if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
+ field.SetUint(parsed)
+ return
+ }
+ }
+ field.SetUint(defaultVal)
+}
+
+func setDurationField(field reflect.Value, envKey, defaultVal string) {
+ if field.Type() == reflect.TypeOf(time.Duration(0)) {
+ defaultDuration, _ := time.ParseDuration(defaultVal)
+ field.Set(reflect.ValueOf(getEnvDuration(envKey, defaultDuration)))
+ }
+}
+
+func setFieldDefault(field reflect.Value, defaultVal string) {
+ switch field.Kind() {
+ case reflect.String:
+ field.SetString(defaultVal)
+ case reflect.Bool:
+ if defaultBool, err := strconv.ParseBool(defaultVal); err == nil {
+ field.SetBool(defaultBool)
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if defaultInt, err := strconv.ParseInt(defaultVal, 10, 64); err == nil {
+ field.SetInt(defaultInt)
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ if defaultUint, err := strconv.ParseUint(defaultVal, 10, 64); err == nil {
+ field.SetUint(defaultUint)
+ }
+ case reflect.Float32, reflect.Float64:
+ if defaultFloat, err := strconv.ParseFloat(defaultVal, 64); err == nil {
+ field.SetFloat(defaultFloat)
+ }
+ default:
+ if field.Type() == reflect.TypeOf(time.Duration(0)) {
+ if defaultDuration, err := time.ParseDuration(defaultVal); err == nil {
+ field.Set(reflect.ValueOf(defaultDuration))
+ }
+ }
+ }
+}
+
+func validateConfigInput(config any) (reflect.Value, reflect.Type, error) {
+ v := reflect.ValueOf(config)
+ if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
+ return reflect.Value{}, nil, fmt.Errorf("config must be a pointer to struct")
+ }
+ elem := v.Elem()
+ return elem, elem.Type(), nil
+}
+
+func parse(config any) error {
+ elem, t, err := validateConfigInput(config)
+ if err != nil {
+ return err
+ }
+
+ for i := range elem.NumField() {
+ field := elem.Field(i)
+ fieldType := t.Field(i)
+
+ if !field.CanSet() {
+ continue
+ }
+
+ envKey := fieldType.Tag.Get("env")
+ defaultVal := fieldType.Tag.Get("default")
+
+ if envKey == "" {
+ continue
+ }
+
+ setFieldFromEnv(field, envKey, defaultVal)
+ }
+
+ return nil
+}
+
+func Defaults[T any](config *T) *T {
+ v := reflect.ValueOf(config)
+ if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
+ return config
+ }
+
+ elem := v.Elem()
+ t := elem.Type()
+ newStruct := reflect.New(t)
+ newElem := newStruct.Elem()
+
+ for i := range elem.NumField() {
+ field := newElem.Field(i)
+ fieldType := t.Field(i)
+
+ if !field.CanSet() {
+ continue
+ }
+
+ defaultVal := fieldType.Tag.Get("default")
+ if defaultVal == "" {
+ continue
+ }
+
+ setFieldDefault(field, defaultVal)
+ }
+
+ return newStruct.Interface().(*T)
+}
diff --git a/config/constants.go b/config/constants.go
new file mode 100644
index 0000000..8cb829d
--- /dev/null
+++ b/config/constants.go
@@ -0,0 +1,7 @@
+package config
+
+const (
+ PAGETITLE_HOME = "Home"
+
+ TEMPLATE_HOME = "home"
+)
diff --git a/config/types.go b/config/types.go
new file mode 100644
index 0000000..a57100f
--- /dev/null
+++ b/config/types.go
@@ -0,0 +1,12 @@
+package config
+
+type ServerConfig struct {
+ Host string `env:"SERVER_HOST" default:"0.0.0.0"`
+ Port int `env:"SERVER_PORT" default:"8080"`
+ IsDevMode bool `env:"SERVER_IS_DEV_MODE" default:"true"`
+}
+
+type GitHubConfig struct {
+ GitHubUsername string `env:"GITHUB_USERNAME"`
+ GitHubToken string `env:"GITHUB_TOKEN"`
+}
diff --git a/controllers/home.go b/controllers/home.go
new file mode 100644
index 0000000..8cb7d06
--- /dev/null
+++ b/controllers/home.go
@@ -0,0 +1,12 @@
+package controllers
+
+import (
+ "eda/config"
+ "eda/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func HomeController(ctx *fiber.Ctx) error {
+ return shortcuts.Render(ctx, config.TEMPLATE_HOME, nil)
+}
diff --git a/eda/main.go b/eda/main.go
new file mode 100644
index 0000000..56ad7fe
--- /dev/null
+++ b/eda/main.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+ "eda/config"
+ "eda/processors"
+ "eda/router"
+ "fmt"
+ "log"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/cors"
+ "github.com/gofiber/fiber/v2/middleware/helmet"
+ "github.com/gofiber/fiber/v2/middleware/logger"
+ "github.com/gofiber/fiber/v2/middleware/recover"
+ "github.com/gofiber/template/django/v3"
+)
+
+func main() {
+ engine := django.New("./templates", ".django")
+ engine.Reload(config.Server.IsDevMode)
+
+ server := fiber.New(fiber.Config{
+ Views: engine,
+ ErrorHandler: serverErrorHandler,
+ })
+ server.Use(recover.New())
+ server.Use(logger.New())
+ server.Use(helmet.New(helmet.Config{
+ CrossOriginEmbedderPolicy: "unsafe-none",
+ }))
+ server.Use(cors.New(cors.Config{
+ AllowOrigins: "*",
+ AllowHeaders: "Origin, Content-Type, Accept",
+ }))
+
+ processors.Initialise(server)
+ router.Initialise(server)
+
+ log.Fatalf("Server failed to start: %v", server.Listen(fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)))
+}
+
+func serverErrorHandler(ctx *fiber.Ctx, err error) error {
+ code := fiber.StatusInternalServerError
+ msg := "Internal Server Error"
+ if e, ok := err.(*fiber.Error); ok {
+ code = e.Code
+ msg = e.Message
+ } else if err != nil {
+ msg = err.Error()
+ }
+ return ctx.Status(code).SendString(msg)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b34e826
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,26 @@
+module eda
+
+go 1.24.5
+
+require (
+ github.com/gofiber/fiber/v2 v2.52.9
+ github.com/gofiber/template/django/v3 v3.1.14
+ github.com/joho/godotenv v1.5.1
+)
+
+require (
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/flosch/pongo2/v6 v6.0.0 // indirect
+ github.com/gofiber/template v1.8.3 // indirect
+ github.com/gofiber/utils v1.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.51.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..3284bdd
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,51 @@
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
+github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
+github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
+github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
+github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
+github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
+github.com/gofiber/template/django/v3 v3.1.14 h1:SvTvs+u5vTZuu1Y2pMUD2NhaGIjBj9FmDA3XD50QBvw=
+github.com/gofiber/template/django/v3 v3.1.14/go.mod h1:gP4vH+T1ajZw7yaejqG1dZVdHQkMC/jPoQbmlG812I0=
+github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
+github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
+github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/processors/meta.go b/processors/meta.go
new file mode 100644
index 0000000..2c931b1
--- /dev/null
+++ b/processors/meta.go
@@ -0,0 +1,10 @@
+package processors
+
+import "github.com/gofiber/fiber/v2"
+
+const defaultTitle = "default"
+
+func MetaContextProcessor(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", defaultTitle)
+ return ctx.Next()
+}
diff --git a/processors/processors.go b/processors/processors.go
new file mode 100644
index 0000000..a07608e
--- /dev/null
+++ b/processors/processors.go
@@ -0,0 +1,7 @@
+package processors
+
+import "github.com/gofiber/fiber/v2"
+
+func Initialise(app *fiber.App) {
+ app.Use(MetaContextProcessor)
+}
diff --git a/router/routes.go b/router/routes.go
new file mode 100644
index 0000000..6915ec6
--- /dev/null
+++ b/router/routes.go
@@ -0,0 +1,12 @@
+package router
+
+import (
+ "eda/controllers"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Initialise(router *fiber.App) {
+ router.Static("/static", "./static")
+ router.Get("/", controllers.HomeController)
+}
diff --git a/templates/home.django b/templates/home.django
new file mode 100644
index 0000000..ec62d17
--- /dev/null
+++ b/templates/home.django
@@ -0,0 +1,4 @@
+{% extends 'layouts/main.django' %}
+{% block content %}
+ <h1>Welcome to Eda</h1>
+{% endblock %}
diff --git a/templates/layouts/main.django b/templates/layouts/main.django
new file mode 100644
index 0000000..713c42a
--- /dev/null
+++ b/templates/layouts/main.django
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <title>{{ Title }} - Eda</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ {% block head %}
+
+ {% endblock %}
+ </head>
+ <body>
+ <main>
+ <aside class="sidebar"></aside>
+ <section class="content">
+ {% block content %}
+
+ {% endblock %}
+ </section>
+ </main>
+
+ <footer>
+ <p>
+ &copy; 2025 Eda. Eda is an <a href="https://github.com/luciferreeves/eda" target="_blank" rel="noopener noreferrer">open-source</a> alternative <a href="https://www.github.com/" target="_blank" rel="noopener noreferrer">GitHub</a> frontend created by <a href="https://shi.foo" target="_blank" rel="noopener noreferrer">@luciferreeves</a>. All rights reserved.
+ </p>
+ </footer>
+ </body>
+ {% block scripts %}
+
+ {% endblock %}
+</html>
diff --git a/utils/shortcuts/render.go b/utils/shortcuts/render.go
new file mode 100644
index 0000000..19635e2
--- /dev/null
+++ b/utils/shortcuts/render.go
@@ -0,0 +1,71 @@
+package shortcuts
+
+import (
+ "reflect"
+ "strings"
+
+ "maps"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Render(ctx *fiber.Ctx, name string, bind any) error {
+ finalData := fiber.Map{}
+
+ ctx.Context().VisitUserValues(func(key []byte, value any) {
+ finalData[string(key)] = value
+ })
+
+ if bind != nil {
+ switch v := bind.(type) {
+ case fiber.Map:
+ maps.Copy(finalData, v)
+ case map[string]any:
+ maps.Copy(finalData, v)
+ default:
+ structData := structToMap(bind)
+ maps.Copy(finalData, structData)
+ }
+ }
+
+ return ctx.Render(name, finalData)
+}
+
+func RenderWithStatus(ctx *fiber.Ctx, name string, bind any, statusCode int) error {
+ ctx.Status(statusCode)
+ return Render(ctx, name, bind)
+}
+
+func structToMap(obj any) map[string]any {
+ result := make(map[string]any)
+
+ v := reflect.ValueOf(obj)
+ if v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+
+ if v.Kind() != reflect.Struct {
+ return result
+ }
+
+ t := v.Type()
+ for i := range v.NumField() {
+ field := t.Field(i)
+ if !field.IsExported() {
+ continue
+ }
+
+ key := field.Name
+ if tag := field.Tag.Get("json"); tag != "" && tag != "-" {
+ if commaIdx := strings.Index(tag, ","); commaIdx > 0 {
+ key = tag[:commaIdx]
+ } else if commaIdx == -1 {
+ key = tag
+ }
+ }
+
+ result[key] = v.Field(i).Interface()
+ }
+
+ return result
+}