Skip to content

Commit

Permalink
✨ Add cow-themed example pages
Browse files Browse the repository at this point in the history
  • Loading branch information
wesen committed Feb 22, 2025
1 parent b14e7c5 commit 73dda76
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/ui-server",
"args": ["examples/pages"],
"args": ["start", "examples/pages", "--log-level", "debug"],
"cwd": "${workspaceFolder}"
}
]
Expand Down
10 changes: 9 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -1199,4 +1199,12 @@ Added file watching capabilities to the UI server for automatic page reloading:
Improved logging in the UI server by:
- Switched to zerolog for structured logging
- Implemented glazed command pattern for better CLI structure
- Added proper log levels and context fields
- Added proper log levels and context fields

## Cow-themed Example Pages

Added new example pages showcasing the UI DSL capabilities with a fun cow theme:
- Added cow-facts.yaml with interactive cow facts and newsletter signup
- Added build-a-cow.yaml with a form to create custom cows
- Added dairy-farm-guide.yaml with farm areas, visitor guidelines and tour booking
- Added cow-quiz.yaml featuring an interactive cow knowledge quiz
2 changes: 2 additions & 0 deletions cmd/ui-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func main() {

startCmd, err := NewStartCommand()
cobra.CheckErr(err)
err = clay.InitViper("ui-server", rootCmd)
cobra.CheckErr(err)

cobraStartCmd, err := cli.BuildCobraCommandFromBareCommand(startCmd)
cobra.CheckErr(err)
Expand Down
49 changes: 48 additions & 1 deletion cmd/ui-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"strings"
"time"

"github.com/go-go-golems/clay/pkg/watcher"
"github.com/rs/zerolog/log"
Expand All @@ -31,6 +32,7 @@ func NewServer(dir string) *Server {
}

// Create a watcher for the pages directory
log.Debug().Str("directory", dir).Msg("Initializing watcher for directory")
w := watcher.NewWatcher(
watcher.WithPaths(dir),
watcher.WithMask("**/*.yaml"),
Expand All @@ -43,12 +45,14 @@ func NewServer(dir string) *Server {
}

func (s *Server) Start(ctx context.Context, port int) error {
log.Debug().Int("port", port).Msg("Starting server")
if err := s.loadPages(); err != nil {
return fmt.Errorf("failed to load pages: %w", err)
}

// Start the file watcher
go func() {
log.Debug().Msg("Starting file watcher")
if err := s.watcher.Run(ctx); err != nil && err != context.Canceled {
log.Error().Err(err).Msg("Watcher error")
}
Expand All @@ -59,7 +63,32 @@ func (s *Server) Start(ctx context.Context, port int) error {
Handler: s.Handler(),
}

return srv.ListenAndServe()
// Start server in a goroutine
serverErr := make(chan error, 1)
go func() {
log.Info().Str("addr", srv.Addr).Msg("Starting server")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
serverErr <- fmt.Errorf("server error: %w", err)
}
close(serverErr)
}()

// Wait for either context cancellation or server error
select {
case err := <-serverErr:
return err
case <-ctx.Done():
log.Info().Msg("Server shutdown initiated")
// Graceful shutdown with timeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()

if err := srv.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("server shutdown error: %w", err)
}
log.Info().Msg("Server shutdown completed")
return nil
}
}

func (s *Server) Handler() http.Handler {
Expand All @@ -81,13 +110,17 @@ func (s *Server) Handler() http.Handler {
}

func (s *Server) loadPages() error {
log.Debug().Str("directory", s.dir).Msg("Loading pages from directory")
return filepath.WalkDir(s.dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
log.Error().Err(err).Str("path", path).Msg("Error walking directory")
return err
}

if !d.IsDir() && strings.HasSuffix(d.Name(), ".yaml") {
log.Debug().Str("path", path).Msg("Found YAML page")
if err := s.loadPage(path); err != nil {
log.Error().Err(err).Str("path", path).Msg("Failed to load page")
return err
}
}
Expand All @@ -96,23 +129,35 @@ func (s *Server) loadPages() error {
}

func (s *Server) loadPage(path string) error {
log.Debug().Str("path", path).Msg("Loading page")
data, err := os.ReadFile(path)
if err != nil {
log.Error().Err(err).Str("path", path).Msg("Failed to read file")
return fmt.Errorf("failed to read file %s: %w", path, err)
}

var def UIDefinition
if err := yaml.Unmarshal(data, &def); err != nil {
log.Error().Err(err).Str("path", path).Msg("Failed to parse YAML")
return fmt.Errorf("failed to parse YAML in %s: %w", path, err)
}

relPath, err := filepath.Rel(s.dir, path)
if err != nil {
log.Error().Err(err).Str("path", path).Msg("Failed to get relative path")
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
}

// Normalize the relative path to use as a URL path
urlPath := "/" + strings.TrimSuffix(relPath, ".yaml")
urlPath = strings.ReplaceAll(urlPath, string(os.PathSeparator), "/")

s.pages[relPath] = def
log.Info().Str("path", relPath).Msg("Loaded page")

// Register the page handler for the URL path
http.HandleFunc(urlPath, s.handlePage(relPath))

return nil
}

Expand All @@ -121,6 +166,7 @@ func (s *Server) handleFileChange(path string) error {
return nil
}

log.Debug().Str("path", path).Msg("File change detected")
log.Info().Str("path", path).Msg("Reloading page")
return s.loadPage(path)
}
Expand All @@ -130,6 +176,7 @@ func (s *Server) handleFileRemove(path string) error {
return nil
}

log.Debug().Str("path", path).Msg("File removal detected")
relPath, err := filepath.Rel(s.dir, path)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
Expand Down
76 changes: 76 additions & 0 deletions examples/pages/cow/build-a-cow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
components:
- title:
id: build-cow-title
content: "🐮 Design Your Dream Cow"

- text:
id: build-intro
content: "Create your perfect bovine companion! Mix and match features to design your unique cow."

- form:
id: cow-builder
components:
- input:
id: cow-name
type: text
placeholder: "Name your cow"
required: true

- list:
type: ul
items:
- "Physical Characteristics":
list:
type: ul
items:
- input:
id: cow-weight
type: number
placeholder: "Weight (kg)"
- input:
id: cow-height
type: number
placeholder: "Height (cm)"

- textarea:
id: cow-description
placeholder: "Describe your cow's personality..."
rows: 4
cols: 50

- text:
content: "Select coat pattern:"

- list:
type: ul
items:
- checkbox:
id: pattern-holstein
label: "Holstein (black and white)"
- checkbox:
id: pattern-jersey
label: "Jersey (solid brown)"
- checkbox:
id: pattern-hereford
label: "Hereford (red and white)"

- text:
content: "Special traits:"

- list:
type: ul
items:
- checkbox:
id: trait-friendly
label: "Extra Friendly"
- checkbox:
id: trait-milk
label: "High Milk Production"
- checkbox:
id: trait-grass
label: "Grass Connoisseur"

- button:
id: create-cow-btn
text: "Create Cow"
type: primary
53 changes: 53 additions & 0 deletions examples/pages/cow/cow-facts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
components:
- title:
id: cow-facts-title
content: "🐄 Fascinating Cow Facts"

- text:
id: intro-text
content: "Discover amazing facts about these gentle giants that have been our companions for thousands of years!"

- list:
type: ul
items:
- "General Facts":
list:
type: ul
items:
- "Cows can sleep while standing up"
- "They have 40,000 jaw movements a day"
- "Each cow has a unique pattern of spots":
button:
id: pattern-info
text: "Learn More"
type: secondary

- "Social Facts":
list:
type: ul
items:
- "Cows have best friends"
- "They form strong social bonds"
- "They can recognize over 100 other cows":
button:
id: social-info
text: "See Research"
type: primary

- form:
id: fact-subscription
components:
- title:
content: "Get Daily Cow Facts!"
- input:
id: email-input
type: email
placeholder: "Enter your email"
required: true
- checkbox:
id: newsletter-check
label: "Also subscribe to our farming newsletter"
- button:
id: subscribe-btn
text: "Subscribe"
type: success
65 changes: 65 additions & 0 deletions examples/pages/cow/cow-quiz.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
components:
- title:
id: quiz-title
content: "🎯 Test Your Cow Knowledge!"

- text:
id: quiz-intro
content: "Think you know your cows? Take this quiz to prove your expertise!"

- form:
id: cow-quiz
components:
- title:
content: "Question 1"
- text:
content: "How many stomachs does a cow have?"
- list:
type: ul
items:
- checkbox:
id: q1-two
label: "Two"
- checkbox:
id: q1-three
label: "Three"
- checkbox:
id: q1-four
label: "Four"

- title:
content: "Question 2"
- text:
content: "Which breed is known for producing the most milk?"
- list:
type: ul
items:
- checkbox:
id: q2-holstein
label: "Holstein-Friesian"
- checkbox:
id: q2-jersey
label: "Jersey"
- checkbox:
id: q2-guernsey
label: "Guernsey"

- title:
content: "Question 3"
- text:
content: "How many liters of water does a dairy cow drink per day?"
- input:
id: q3-water
type: number
placeholder: "Enter number of liters"
required: true

- button:
id: submit-quiz
text: "Submit Answers"
type: primary

- button:
id: reset-quiz
text: "Start Over"
type: secondary
Loading

0 comments on commit 73dda76

Please sign in to comment.