Add templating
This commit is contained in:
373
.github/go.instructions.md
vendored
Normal file
373
.github/go.instructions.md
vendored
Normal file
@@ -0,0 +1,373 @@
|
||||
---
|
||||
description: 'Instructions for writing Go code following idiomatic Go practices and community standards'
|
||||
applyTo: '**/*.go,**/go.mod,**/go.sum'
|
||||
---
|
||||
|
||||
# Go Development Instructions
|
||||
|
||||
Follow idiomatic Go practices and community standards when writing Go code. These instructions are based on [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments), and [Google's Go Style Guide](https://google.github.io/styleguide/go/).
|
||||
|
||||
## General Instructions
|
||||
|
||||
- Write simple, clear, and idiomatic Go code
|
||||
- Favor clarity and simplicity over cleverness
|
||||
- Follow the principle of least surprise
|
||||
- Keep the happy path left-aligned (minimize indentation)
|
||||
- Return early to reduce nesting
|
||||
- Prefer early return over if-else chains; use `if condition { return }` pattern to avoid else blocks
|
||||
- Make the zero value useful
|
||||
- Write self-documenting code with clear, descriptive names
|
||||
- Document exported types, functions, methods, and packages
|
||||
- Use Go modules for dependency management
|
||||
- Leverage the Go standard library instead of reinventing the wheel (e.g., use `strings.Builder` for string concatenation, `filepath.Join` for path construction)
|
||||
- Prefer standard library solutions over custom implementations when functionality exists
|
||||
- Write comments in English by default; translate only upon user request
|
||||
- Avoid using emoji in code and comments
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Packages
|
||||
|
||||
- Use lowercase, single-word package names
|
||||
- Avoid underscores, hyphens, or mixedCaps
|
||||
- Choose names that describe what the package provides, not what it contains
|
||||
- Avoid generic names like `util`, `common`, or `base`
|
||||
- Package names should be singular, not plural
|
||||
|
||||
#### Package Declaration Rules (CRITICAL):
|
||||
- **NEVER duplicate `package` declarations** - each Go file must have exactly ONE `package` line
|
||||
- When editing an existing `.go` file:
|
||||
- **PRESERVE** the existing `package` declaration - do not add another one
|
||||
- If you need to replace the entire file content, start with the existing package name
|
||||
- When creating a new `.go` file:
|
||||
- **BEFORE writing any code**, check what package name other `.go` files in the same directory use
|
||||
- Use the SAME package name as existing files in that directory
|
||||
- If it's a new directory, use the directory name as the package name
|
||||
- Write **exactly one** `package <name>` line at the very top of the file
|
||||
- When using file creation or replacement tools:
|
||||
- **ALWAYS verify** the target file doesn't already have a `package` declaration before adding one
|
||||
- If replacing file content, include only ONE `package` declaration in the new content
|
||||
- **NEVER** create files with multiple `package` lines or duplicate declarations
|
||||
|
||||
### Variables and Functions
|
||||
|
||||
- Use mixedCaps or MixedCaps (camelCase) rather than underscores
|
||||
- Keep names short but descriptive
|
||||
- Use single-letter variables only for very short scopes (like loop indices)
|
||||
- Exported names start with a capital letter
|
||||
- Unexported names start with a lowercase letter
|
||||
- Avoid stuttering (e.g., avoid `http.HTTPServer`, prefer `http.Server`)
|
||||
|
||||
### Interfaces
|
||||
|
||||
- Name interfaces with -er suffix when possible (e.g., `Reader`, `Writer`, `Formatter`)
|
||||
- Single-method interfaces should be named after the method (e.g., `Read` → `Reader`)
|
||||
- Keep interfaces small and focused
|
||||
|
||||
### Constants
|
||||
|
||||
- Use MixedCaps for exported constants
|
||||
- Use mixedCaps for unexported constants
|
||||
- Group related constants using `const` blocks
|
||||
- Consider using typed constants for better type safety
|
||||
|
||||
## Code Style and Formatting
|
||||
|
||||
### Formatting
|
||||
|
||||
- Always use `gofmt` to format code
|
||||
- Use `goimports` to manage imports automatically
|
||||
- Keep line length reasonable (no hard limit, but consider readability)
|
||||
- Add blank lines to separate logical groups of code
|
||||
|
||||
### Comments
|
||||
|
||||
- Strive for self-documenting code; prefer clear variable names, function names, and code structure over comments
|
||||
- Write comments only when necessary to explain complex logic, business rules, or non-obvious behavior
|
||||
- Write comments in complete sentences in English by default
|
||||
- Translate comments to other languages only upon specific user request
|
||||
- Start sentences with the name of the thing being described
|
||||
- Package comments should start with "Package [name]"
|
||||
- Use line comments (`//`) for most comments
|
||||
- Use block comments (`/* */`) sparingly, mainly for package documentation
|
||||
- Document why, not what, unless the what is complex
|
||||
- Avoid emoji in comments and code
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Check errors immediately after the function call
|
||||
- Don't ignore errors using `_` unless you have a good reason (document why)
|
||||
- Wrap errors with context using `fmt.Errorf` with `%w` verb
|
||||
- Create custom error types when you need to check for specific errors
|
||||
- Place error returns as the last return value
|
||||
- Name error variables `err`
|
||||
- Keep error messages lowercase and don't end with punctuation
|
||||
|
||||
## Architecture and Project Structure
|
||||
|
||||
### Package Organization
|
||||
|
||||
- Follow standard Go project layout conventions
|
||||
- Keep `main` packages in `cmd/` directory
|
||||
- Put reusable packages in `pkg/` or `internal/`
|
||||
- Use `internal/` for packages that shouldn't be imported by external projects
|
||||
- Group related functionality into packages
|
||||
- Avoid circular dependencies
|
||||
|
||||
### Dependency Management
|
||||
|
||||
- Use Go modules (`go.mod` and `go.sum`)
|
||||
- Keep dependencies minimal
|
||||
- Regularly update dependencies for security patches
|
||||
- Use `go mod tidy` to clean up unused dependencies
|
||||
- Vendor dependencies only when necessary
|
||||
|
||||
## Type Safety and Language Features
|
||||
|
||||
### Type Definitions
|
||||
|
||||
- Define types to add meaning and type safety
|
||||
- Use struct tags for JSON, XML, database mappings
|
||||
- Prefer explicit type conversions
|
||||
- Use type assertions carefully and check the second return value
|
||||
- Prefer generics over unconstrained types; when an unconstrained type is truly needed, use the predeclared alias `any` instead of `interface{}` (Go 1.18+)
|
||||
|
||||
### Pointers vs Values
|
||||
|
||||
- Use pointer receivers for large structs or when you need to modify the receiver
|
||||
- Use value receivers for small structs and when immutability is desired
|
||||
- Use pointer parameters when you need to modify the argument or for large structs
|
||||
- Use value parameters for small structs and when you want to prevent modification
|
||||
- Be consistent within a type's method set
|
||||
- Consider the zero value when choosing pointer vs value receivers
|
||||
|
||||
### Interfaces and Composition
|
||||
|
||||
- Accept interfaces, return concrete types
|
||||
- Keep interfaces small (1-3 methods is ideal)
|
||||
- Use embedding for composition
|
||||
- Define interfaces close to where they're used, not where they're implemented
|
||||
- Don't export interfaces unless necessary
|
||||
|
||||
## Concurrency
|
||||
|
||||
### Goroutines
|
||||
|
||||
- Be cautious about creating goroutines in libraries; prefer letting the caller control concurrency
|
||||
- If you must create goroutines in libraries, provide clear documentation and cleanup mechanisms
|
||||
- Always know how a goroutine will exit
|
||||
- Use `sync.WaitGroup` or channels to wait for goroutines
|
||||
- Avoid goroutine leaks by ensuring cleanup
|
||||
|
||||
### Channels
|
||||
|
||||
- Use channels to communicate between goroutines
|
||||
- Don't communicate by sharing memory; share memory by communicating
|
||||
- Close channels from the sender side, not the receiver
|
||||
- Use buffered channels when you know the capacity
|
||||
- Use `select` for non-blocking operations
|
||||
|
||||
### Synchronization
|
||||
|
||||
- Use `sync.Mutex` for protecting shared state
|
||||
- Keep critical sections small
|
||||
- Use `sync.RWMutex` when you have many readers
|
||||
- Choose between channels and mutexes based on the use case: use channels for communication, mutexes for protecting state
|
||||
- Use `sync.Once` for one-time initialization
|
||||
- WaitGroup usage by Go version:
|
||||
- If `go >= 1.25` in `go.mod`, use the new `WaitGroup.Go` method ([documentation](https://pkg.go.dev/sync#WaitGroup)):
|
||||
```go
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(task1)
|
||||
wg.Go(task2)
|
||||
wg.Wait()
|
||||
```
|
||||
- If `go < 1.25`, use the classic `Add`/`Done` pattern
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Creating Errors
|
||||
|
||||
- Use `errors.New` for simple static errors
|
||||
- Use `fmt.Errorf` for dynamic errors
|
||||
- Create custom error types for domain-specific errors
|
||||
- Export error variables for sentinel errors
|
||||
- Use `errors.Is` and `errors.As` for error checking
|
||||
|
||||
### Error Propagation
|
||||
|
||||
- Add context when propagating errors up the stack
|
||||
- Don't log and return errors (choose one)
|
||||
- Handle errors at the appropriate level
|
||||
- Consider using structured errors for better debugging
|
||||
|
||||
## API Design
|
||||
|
||||
### HTTP Handlers
|
||||
|
||||
- Use `http.HandlerFunc` for simple handlers
|
||||
- Implement `http.Handler` for handlers that need state
|
||||
- Use middleware for cross-cutting concerns
|
||||
- Set appropriate status codes and headers
|
||||
- Handle errors gracefully and return appropriate error responses
|
||||
- Router usage by Go version:
|
||||
- If `go >= 1.22`, prefer the enhanced `net/http` `ServeMux` with pattern-based routing and method matching
|
||||
- If `go < 1.22`, use the classic `ServeMux` and handle methods/paths manually (or use a third-party router when justified)
|
||||
|
||||
### JSON APIs
|
||||
|
||||
- Use struct tags to control JSON marshaling
|
||||
- Validate input data
|
||||
- Use pointers for optional fields
|
||||
- Consider using `json.RawMessage` for delayed parsing
|
||||
- Handle JSON errors appropriately
|
||||
|
||||
### HTTP Clients
|
||||
|
||||
- Keep the client struct focused on configuration and dependencies only (e.g., base URL, `*http.Client`, auth, default headers). It must not store per-request state
|
||||
- Do not store or cache `*http.Request` inside the client struct, and do not persist request-specific state across calls; instead, construct a fresh request per method invocation
|
||||
- Methods should accept `context.Context` and input parameters, assemble the `*http.Request` locally (or via a short-lived builder/helper created per call), then call `c.httpClient.Do(req)`
|
||||
- If request-building logic is reused, factor it into unexported helper functions or a per-call builder type; never keep `http.Request` (URL params, body, headers) as fields on the long-lived client
|
||||
- Ensure the underlying `*http.Client` is configured (timeouts, transport) and is safe for concurrent use; avoid mutating `Transport` after first use
|
||||
- Always set headers on the request instance you’re sending, and close response bodies (`defer resp.Body.Close()`), handling errors appropriately
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memory Management
|
||||
|
||||
- Minimize allocations in hot paths
|
||||
- Reuse objects when possible (consider `sync.Pool`)
|
||||
- Use value receivers for small structs
|
||||
- Preallocate slices when size is known
|
||||
- Avoid unnecessary string conversions
|
||||
|
||||
### I/O: Readers and Buffers
|
||||
|
||||
- Most `io.Reader` streams are consumable once; reading advances state. Do not assume a reader can be re-read without special handling
|
||||
- If you must read data multiple times, buffer it once and recreate readers on demand:
|
||||
- Use `io.ReadAll` (or a limited read) to obtain `[]byte`, then create fresh readers via `bytes.NewReader(buf)` or `bytes.NewBuffer(buf)` for each reuse
|
||||
- For strings, use `strings.NewReader(s)`; you can `Seek(0, io.SeekStart)` on `*bytes.Reader` to rewind
|
||||
- For HTTP requests, do not reuse a consumed `req.Body`. Instead:
|
||||
- Keep the original payload as `[]byte` and set `req.Body = io.NopCloser(bytes.NewReader(buf))` before each send
|
||||
- Prefer configuring `req.GetBody` so the transport can recreate the body for redirects/retries: `req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(buf)), nil }`
|
||||
- To duplicate a stream while reading, use `io.TeeReader` (copy to a buffer while passing through) or write to multiple sinks with `io.MultiWriter`
|
||||
- Reusing buffered readers: call `(*bufio.Reader).Reset(r)` to attach to a new underlying reader; do not expect it to “rewind” unless the source supports seeking
|
||||
- For large payloads, avoid unbounded buffering; consider streaming, `io.LimitReader`, or on-disk temporary storage to control memory
|
||||
|
||||
- Use `io.Pipe` to stream without buffering the whole payload:
|
||||
- Write to `*io.PipeWriter` in a separate goroutine while the reader consumes
|
||||
- Always close the writer; use `CloseWithError(err)` on failures
|
||||
- `io.Pipe` is for streaming, not rewinding or making readers reusable
|
||||
|
||||
- **Warning:** When using `io.Pipe` (especially with multipart writers), all writes must be performed in strict, sequential order. Do not write concurrently or out of order—multipart boundaries and chunk order must be preserved. Out-of-order or parallel writes can corrupt the stream and result in errors.
|
||||
|
||||
- Streaming multipart/form-data with `io.Pipe`:
|
||||
- `pr, pw := io.Pipe()`; `mw := multipart.NewWriter(pw)`; use `pr` as the HTTP request body
|
||||
- Set `Content-Type` to `mw.FormDataContentType()`
|
||||
- In a goroutine: write all parts to `mw` in the correct order; on error `pw.CloseWithError(err)`; on success `mw.Close()` then `pw.Close()`
|
||||
- Do not store request/in-flight form state on a long-lived client; build per call
|
||||
- Streamed bodies are not rewindable; for retries/redirects, buffer small payloads or provide `GetBody`
|
||||
|
||||
### Profiling
|
||||
|
||||
- Use built-in profiling tools (`pprof`)
|
||||
- Benchmark critical code paths
|
||||
- Profile before optimizing
|
||||
- Focus on algorithmic improvements first
|
||||
- Consider using `testing.B` for benchmarks
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Keep tests in the same package (white-box testing)
|
||||
- Use `_test` package suffix for black-box testing
|
||||
- Name test files with `_test.go` suffix
|
||||
- Place test files next to the code they test
|
||||
|
||||
### Writing Tests
|
||||
|
||||
- Use table-driven tests for multiple test cases
|
||||
- Name tests descriptively using `Test_functionName_scenario`
|
||||
- Use subtests with `t.Run` for better organization
|
||||
- Test both success and error cases
|
||||
- Consider using `testify` or similar libraries when they add value, but don't over-complicate simple tests
|
||||
|
||||
### Test Helpers
|
||||
|
||||
- Mark helper functions with `t.Helper()`
|
||||
- Create test fixtures for complex setup
|
||||
- Use `testing.TB` interface for functions used in tests and benchmarks
|
||||
- Clean up resources using `t.Cleanup()`
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Input Validation
|
||||
|
||||
- Validate all external input
|
||||
- Use strong typing to prevent invalid states
|
||||
- Sanitize data before using in SQL queries
|
||||
- Be careful with file paths from user input
|
||||
- Validate and escape data for different contexts (HTML, SQL, shell)
|
||||
|
||||
### Cryptography
|
||||
|
||||
- Use standard library crypto packages
|
||||
- Don't implement your own cryptography
|
||||
- Use crypto/rand for random number generation
|
||||
- Store passwords using bcrypt, scrypt, or argon2 (consider golang.org/x/crypto for additional options)
|
||||
- Use TLS for network communication
|
||||
|
||||
## Documentation
|
||||
|
||||
### Code Documentation
|
||||
|
||||
- Prioritize self-documenting code through clear naming and structure
|
||||
- Document all exported symbols with clear, concise explanations
|
||||
- Start documentation with the symbol name
|
||||
- Write documentation in English by default
|
||||
- Use examples in documentation when helpful
|
||||
- Keep documentation close to code
|
||||
- Update documentation when code changes
|
||||
- Avoid emoji in documentation and comments
|
||||
|
||||
### README and Documentation Files
|
||||
|
||||
- Include clear setup instructions
|
||||
- Document dependencies and requirements
|
||||
- Provide usage examples
|
||||
- Document configuration options
|
||||
- Include troubleshooting section
|
||||
|
||||
## Tools and Development Workflow
|
||||
|
||||
### Essential Tools
|
||||
|
||||
- `go fmt`: Format code
|
||||
- `go vet`: Find suspicious constructs
|
||||
- `golangci-lint`: Additional linting (golint is deprecated)
|
||||
- `go test`: Run tests
|
||||
- `go mod`: Manage dependencies
|
||||
- `go generate`: Code generation
|
||||
|
||||
### Development Practices
|
||||
|
||||
- Run tests before committing
|
||||
- Use pre-commit hooks for formatting and linting
|
||||
- Keep commits focused and atomic
|
||||
- Write meaningful commit messages
|
||||
- Review diffs before committing
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
- Not checking errors
|
||||
- Ignoring race conditions
|
||||
- Creating goroutine leaks
|
||||
- Not using defer for cleanup
|
||||
- Modifying maps concurrently
|
||||
- Not understanding nil interfaces vs nil pointers
|
||||
- Forgetting to close resources (files, connections)
|
||||
- Using global variables unnecessarily
|
||||
- Over-using unconstrained types (e.g., `any`); prefer specific types or generic type parameters with constraints. If an unconstrained type is required, use `any` rather than `interface{}`
|
||||
- Not considering the zero value of types
|
||||
- **Creating duplicate `package` declarations** - this is a compile error; always check existing files before adding package declarations
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -36,7 +37,7 @@ type Config struct {
|
||||
// Server hosts the Luxtools HTTP handlers.
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
indexPage []byte
|
||||
templates *template.Template
|
||||
staticFS fs.FS
|
||||
mediaFS http.FileSystem
|
||||
|
||||
@@ -60,9 +61,10 @@ func New(cfg Config) (*Server, error) {
|
||||
return nil, fmt.Errorf("media directory must be a directory: %s", mediaDir)
|
||||
}
|
||||
|
||||
indexPage, err := webbundle.Content.ReadFile("templates/index.html")
|
||||
// Parse all templates
|
||||
tmpl, err := template.ParseFS(webbundle.Content, "templates/*.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read index template: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse templates: %w", err)
|
||||
}
|
||||
|
||||
staticFS, err := fs.Sub(webbundle.Content, "static")
|
||||
@@ -72,7 +74,7 @@ func New(cfg Config) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
mux: http.NewServeMux(),
|
||||
indexPage: indexPage,
|
||||
templates: tmpl,
|
||||
staticFS: staticFS,
|
||||
mediaFS: http.Dir(mediaDir),
|
||||
}
|
||||
@@ -103,7 +105,12 @@ func (s *Server) registerRoutes() {
|
||||
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
s.sendHTML(w, http.StatusOK, s.indexPage)
|
||||
data := DefaultMenuBar()
|
||||
s.renderTemplate(w, http.StatusOK, "index.html", data)
|
||||
case "/admin":
|
||||
data := AdminMenuBar()
|
||||
data.Title = "Admin Panel"
|
||||
s.renderTemplate(w, http.StatusOK, "admin.html", data)
|
||||
case "/counter":
|
||||
s.handleCounter(w, r)
|
||||
case "/time":
|
||||
@@ -127,6 +134,16 @@ func (s *Server) handleCounter(w http.ResponseWriter, r *http.Request) {
|
||||
s.sendHTMLString(w, http.StatusOK, renderCounter(count))
|
||||
}
|
||||
|
||||
func (s *Server) renderTemplate(w http.ResponseWriter, status int, name string, data interface{}) {
|
||||
w.Header().Set("Content-Type", htmlContentType)
|
||||
w.WriteHeader(status)
|
||||
|
||||
if err := s.templates.ExecuteTemplate(w, name, data); err != nil {
|
||||
log.Printf("template execution failed: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) sendHTML(w http.ResponseWriter, status int, body []byte) {
|
||||
w.Header().Set("Content-Type", htmlContentType)
|
||||
w.WriteHeader(status)
|
||||
|
||||
86
internal/server/template_data.go
Normal file
86
internal/server/template_data.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package server
|
||||
|
||||
import "html/template"
|
||||
|
||||
// This file defines the data structures and helper functions for the template system.
|
||||
//
|
||||
// The template system uses Go's html/template package to provide:
|
||||
// - Dynamic menu bars that can be customized per page
|
||||
// - Template inheritance via layout.html
|
||||
// - Type-safe data passing via structs
|
||||
//
|
||||
// To create a new page with a custom menu:
|
||||
// 1. Create a PageData struct with your menu configuration
|
||||
// 2. Call s.renderTemplate(w, status, "yourpage.html", data)
|
||||
// 3. In yourpage.html, use {{template "layout" .}} and define content blocks
|
||||
//
|
||||
// See TEMPLATE_GUIDE.md for detailed examples and usage patterns.
|
||||
|
||||
// PageData holds the data passed to page templates.
|
||||
type PageData struct {
|
||||
Title string
|
||||
MenuGroups []MenuGroup
|
||||
ShowClock bool
|
||||
}
|
||||
|
||||
// MenuGroup represents a dropdown menu in the navbar.
|
||||
type MenuGroup struct {
|
||||
LabelHTML template.HTML
|
||||
Items []MenuItem
|
||||
}
|
||||
|
||||
// MenuItem represents a single menu item.
|
||||
type MenuItem struct {
|
||||
LabelHTML template.HTML
|
||||
URL string
|
||||
IsDivider bool
|
||||
}
|
||||
|
||||
// DefaultMenuBar returns the standard menu configuration.
|
||||
func DefaultMenuBar() PageData {
|
||||
return PageData{
|
||||
ShowClock: true,
|
||||
MenuGroups: []MenuGroup{
|
||||
{
|
||||
LabelHTML: template.HTML(`<span class="red-168-text">F</span>ile`),
|
||||
Items: []MenuItem{
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">N</span>ew`), URL: "#!"},
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">O</span>pen`), URL: "#!"},
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">S</span>ave`), URL: "#!"},
|
||||
{LabelHTML: template.HTML(`Save <span class="red-168-text">A</span>s`), URL: "#!"},
|
||||
{IsDivider: true},
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">E</span>xit`), URL: "#!"},
|
||||
},
|
||||
},
|
||||
{
|
||||
LabelHTML: template.HTML(`<span class="red-168-text">E</span>dit`),
|
||||
Items: []MenuItem{
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">C</span>ut`), URL: "#!"},
|
||||
{LabelHTML: template.HTML(`C<span class="red-168-text">o</span>py`), URL: "#!"},
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">P</span>aste`), URL: "#!"},
|
||||
},
|
||||
},
|
||||
{
|
||||
LabelHTML: template.HTML(`<span class="red-168-text">H</span>elp`),
|
||||
Items: []MenuItem{
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">D</span>ocumentation`), URL: "#!"},
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">A</span>bout`), URL: "#!"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AdminMenuBar returns a menu configuration with admin options.
|
||||
func AdminMenuBar() PageData {
|
||||
data := DefaultMenuBar()
|
||||
data.MenuGroups = append(data.MenuGroups, MenuGroup{
|
||||
LabelHTML: template.HTML(`<span class="red-168-text">A</span>dmin`),
|
||||
Items: []MenuItem{
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">U</span>sers`), URL: "/admin/users"},
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">S</span>ettings`), URL: "/admin/settings"},
|
||||
{LabelHTML: template.HTML(`<span class="red-168-text">L</span>ogs`), URL: "/admin/logs"},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -4,5 +4,5 @@ import "embed"
|
||||
|
||||
// Content holds the HTML templates and static assets for the web UI.
|
||||
//
|
||||
//go:embed templates/index.html static
|
||||
//go:embed templates/* static
|
||||
var Content embed.FS
|
||||
|
||||
28
web/templates/admin.html
Normal file
28
web/templates/admin.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Admin Panel · Luxtools{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Admin Panel</legend>
|
||||
<p class="tui-text-white">
|
||||
This is an example admin page with a different menu bar that includes admin-specific options.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-yellow">System Status</h2>
|
||||
<div class="tui-panel">
|
||||
<p><strong>Server:</strong> Running</p>
|
||||
<p><strong>Users:</strong> 42 active</p>
|
||||
<p><strong>Uptime:</strong> 7 days, 3 hours</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-yellow">Quick Actions</h2>
|
||||
<button class="tui-button">View Logs</button>
|
||||
<button class="tui-button">Manage Users</button>
|
||||
<button class="tui-button">Settings</button>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
@@ -1,57 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Luxtools · Nim + HTMX demo</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/lib/tuicss/tuicss.min.css"
|
||||
/>
|
||||
<script src="/static/lib/htmx.2.0.7.min.js" defer></script>
|
||||
<script src="/static/lib/tuicss/tuicss.min.js" defer></script>
|
||||
</head>
|
||||
<body class="tui-bg-black">
|
||||
<main class="tui-container tui-window" style="margin-top: 2rem;">
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Luxtools · Go + HTMX demo{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Luxtools control panel</legend>
|
||||
<p class="tui-text-white">
|
||||
Luxtools demonstrates a Nim backend compiled into a single binary. The UI
|
||||
uses <strong>TUI.CSS</strong> for a retro feel and <strong>HTMX</strong> for
|
||||
partial page updates without JavaScript glue code.
|
||||
</p>
|
||||
<p class="tui-text-white">
|
||||
Luxtools demonstrates a Go backend compiled into a single binary. The UI
|
||||
uses <strong>TUI.CSS</strong> for a retro feel and <strong>HTMX</strong> for
|
||||
partial page updates without JavaScript glue code.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Interactive counter</h2>
|
||||
<p class="tui-text-silver">
|
||||
Click the button to trigger an <code>hx-post</code> request. The response
|
||||
replaces only the counter panel.
|
||||
</p>
|
||||
<div
|
||||
id="counter"
|
||||
hx-get="/counter"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<div class="tui-panel tui-panel-inline">Loading…</div>
|
||||
</div>
|
||||
</section>
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Interactive counter</h2>
|
||||
<p class="tui-text-silver">
|
||||
Click the button to trigger an <code>hx-post</code> request. The response
|
||||
replaces only the counter panel.
|
||||
</p>
|
||||
<div
|
||||
id="counter"
|
||||
hx-get="/counter"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<div class="tui-panel tui-panel-inline">Loading…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Server time</h2>
|
||||
<p class="tui-text-silver">
|
||||
A periodic <code>hx-get</code> refresh keeps this panel in sync with the
|
||||
server clock.
|
||||
</p>
|
||||
<div
|
||||
id="server-time"
|
||||
hx-get="/time"
|
||||
hx-trigger="load, every 5s"
|
||||
>
|
||||
<div class="tui-panel tui-panel-inline">Loading…</div>
|
||||
</div>
|
||||
</section>
|
||||
</fieldset>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Server time</h2>
|
||||
<p class="tui-text-silver">
|
||||
A periodic <code>hx-get</code> refresh keeps this panel in sync with the
|
||||
server clock.
|
||||
</p>
|
||||
<div
|
||||
id="server-time"
|
||||
hx-get="/time"
|
||||
hx-trigger="load, every 5s"
|
||||
>
|
||||
<div class="tui-panel tui-panel-inline">Loading…</div>
|
||||
</div>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
|
||||
19
web/templates/layout.html
Normal file
19
web/templates/layout.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{{define "layout"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{block "title" .}}Luxtools{{end}}</title>
|
||||
<link rel="stylesheet" href="/static/lib/tuicss/tuicss.min.css" />
|
||||
<script src="/static/lib/htmx.2.0.7.min.js" defer></script>
|
||||
<script src="/static/lib/tuicss/tuicss.min.js" defer></script>
|
||||
</head>
|
||||
<body class="tui-bg-black">
|
||||
{{block "menubar" .}}{{end}}
|
||||
<main class="tui-container tui-window" style="margin-top: 2rem;">
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
25
web/templates/menubar.html
Normal file
25
web/templates/menubar.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{{define "menubar"}}
|
||||
<nav class="tui-nav">
|
||||
<ul>
|
||||
{{range .MenuGroups}}
|
||||
<li class="tui-dropdown">
|
||||
{{.LabelHTML}}
|
||||
<div class="tui-dropdown-content">
|
||||
<ul>
|
||||
{{range .Items}}
|
||||
{{if .IsDivider}}
|
||||
<li class="tui-divider"></li>
|
||||
{{else}}
|
||||
<li><a href="{{.URL}}">{{.LabelHTML}}</a></li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{if .ShowClock}}
|
||||
<span class="tui-datetime" data-format="h:m:s a"></span>
|
||||
{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user