diff --git a/examples/README.md b/examples/README.md index d7f2a55..74c5e5c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,74 @@ -To run these examples: +# Structpages Examples + +This directory contains examples demonstrating various features of the structpages library. + +## Available Examples + +### 1. Simple +Basic routing and page structure demonstration. +- Simple struct-based routing +- Basic templ integration +- URL generation with `urlFor` + +### 2. HTMX +HTMX integration with partial rendering. +- HTMX partial component rendering +- Custom error handling for HTMX requests +- Middleware usage +- Dynamic content updates + +### 3. Todo +Complete CRUD application with forms. +- Full CRUD operations +- Form handling with struct tags +- Custom ServeHTTP handlers +- In-memory data storage +- HTMX interactions + +### 4. Blog-Admin +Advanced blog platform with admin panel demonstrating: +- **Dependency Injection**: Multiple services (DB, Auth, Session, Config) +- **Nested Routing**: 3-level deep route structure (`/admin/posts/{id}/edit`) +- **Authentication & Authorization**: Session-based auth with role-based access +- **Middleware Patterns**: Global and per-route middleware +- **Props Pattern**: Type-safe data loading with complex queries +- **Advanced HTMX**: Custom PageConfig, partial rendering, auto-save +- **Form Handling**: Structured form parsing with validation +- **Database Integration**: SQLite with transactions and indexes +- **Component Composition**: Reusable Templ components +- **API Endpoints**: JSON APIs alongside HTML pages +- **Query Parameters**: Pagination, filtering, search +- **Real-time Features**: Auto-save, live updates +- **Error Handling**: Custom error pages and graceful degradation + +## Running the Examples + +Each example is a standalone Go module. To run an example: ```shell +# Navigate to example directory cd examples/simple/ + +# Download dependencies +go mod download + +# Generate templ files +templ generate + +# Run the server +go run . + +# Or use templ's watch mode for development templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." ``` + +Open http://localhost:8080 in your browser. + +## Learning Path + +1. Start with **simple** to understand basic routing +2. Move to **htmx** to learn about partial rendering +3. Study **todo** for form handling and CRUD operations +4. Explore **blog-admin** for production-ready patterns + +Each example builds on concepts from the previous ones, demonstrating increasingly sophisticated use of structpages features. \ No newline at end of file diff --git a/examples/blog-admin/.gitignore b/examples/blog-admin/.gitignore new file mode 100644 index 0000000..efe05cb --- /dev/null +++ b/examples/blog-admin/.gitignore @@ -0,0 +1,17 @@ +# Binary +blog-admin + +# Logs +*.log + +# Database +*.db + +# Session/cookie files +cookies.txt + +# Generated templ files +*_templ.go + +# OS files +.DS_Store \ No newline at end of file diff --git a/examples/blog-admin/README.md b/examples/blog-admin/README.md new file mode 100644 index 0000000..ad076af --- /dev/null +++ b/examples/blog-admin/README.md @@ -0,0 +1,170 @@ +# Blog Admin Example + +This is a comprehensive example demonstrating advanced features of the structpages framework. It implements a full-featured blog platform with public-facing pages and an admin panel. + +## Features Demonstrated + +### 1. **Dependency Injection** +- Multiple services injected via `MountPages`: Store, AuthService, SessionManager, FormDecoder, Config +- Type-based injection with automatic parameter resolution +- Services available in Props methods, ServeHTTP handlers, and middleware + +### 2. **Nested Routing Structure** +- Three-level deep routes (e.g., `/admin/posts/{id}/edit`) +- Struct embedding for hierarchical organization +- Clean URL patterns with path parameters + +### 3. **Authentication & Authorization** +- Session-based authentication with bcrypt password hashing +- Role-based access control (admin, author, reader) +- Protected routes using middleware composition +- Login/logout flow with redirect support + +### 4. **Advanced Middleware Patterns** +- Global middleware (session loading, logging) +- Per-route middleware (authentication, CSRF protection) +- Middleware composition with proper execution order +- Context-aware middleware with PageNode access + +### 5. **Props Pattern** +- Type-safe props with complex data loading +- Dependency injection in Props methods +- Error handling and data validation +- Efficient database queries with relationship loading + +### 6. **HTMX Integration** +- Partial component rendering +- Custom PageConfig for different HX-Target values +- Progressive enhancement patterns +- Real-time updates (publish/unpublish) +- Auto-save functionality +- Form submissions with URL updates + +### 7. **Form Handling** +- Structured form parsing with go-playground/form +- Multi-value form fields (categories, tags) +- File upload handling (media library) +- Validation and error display + +### 8. **Database Integration** +- SQLite with proper schema and indexes +- Transaction support for complex operations +- Efficient queries with pagination +- Analytics and view tracking + +### 9. **Component Composition** +- Reusable Templ components (layouts, cards, forms) +- Conditional rendering based on user roles +- Dynamic component selection +- Shared UI components library + +### 10. **URL Generation** +- Type-safe URL generation with `urlFor` +- Support for path parameters +- Query string building with `join` helper +- HTMX-aware URL handling + +### 11. **Error Handling** +- Custom error handler with status codes +- Graceful degradation +- User-friendly error messages +- Proper HTTP status responses + +### 12. **Advanced ServeHTTP Patterns** +- Error-returning ServeHTTP (buffered response) +- Standard http.Handler interface (direct write) +- API endpoints with JSON responses +- Mixed content types handling + +## Project Structure + +``` +blog-admin/ +├── main.go # Application entry point +├── routes.go # Route definitions and middleware +├── models.go # Data models and database schema +├── store.go # Database operations +├── auth.go # Authentication service +├── components.templ # Reusable UI components +├── pages.templ # Public pages (home, post, search, login) +├── admin_pages.templ # Admin dashboard and post management +├── admin_users.templ # User management and settings +├── api_pages.templ # API endpoints and advanced patterns +└── static/ # CSS and static assets + ├── styles.css # Public site styles + └── admin.css # Admin panel styles +``` + +## Running the Example + +1. Install dependencies: +```bash +go mod download +``` + +2. Generate Templ files: +```bash +templ generate +``` + +3. Run the application: +```bash +go run . +``` + +4. Access the application: +- Public site: http://localhost:8080 +- Admin panel: http://localhost:8080/admin +- Login credentials: admin / admin123 + +## Key Patterns to Study + +### Dependency Injection +See how services are registered in `main.go` and used throughout the application: +```go +if err := sp.MountPages(r, pages{}, "/", "Blog", store, auth, sessionManager, formDecoder, config); err != nil { + log.Fatal(err) +} +``` + +### Nested Routes with Middleware +Check `routes.go` for the hierarchical structure and middleware application: +```go +type adminPages struct { + dashboard `route:"/{$} Dashboard"` + posts adminPostPages `route:"/posts Posts"` +} + +func (a adminPages) Middlewares(auth *AuthService) []structpages.MiddlewareFunc { + return []structpages.MiddlewareFunc{ + requireAuthMiddleware(auth), + requireAdminMiddleware(auth), + } +} +``` + +### Props Pattern with Data Loading +See `pages.templ` for examples of loading complex data: +```go +func (p postPage) Props(r *http.Request, store *Store, auth *AuthService) (postPageProps, error) { + slug := r.PathValue("slug") + post, err := store.GetPostBySlug(slug) + // ... load related data +} +``` + +### HTMX Partial Rendering +Check `api_pages.templ` for custom PageConfig: +```go +func (s searchPage) PageConfig(r *http.Request) (string, error) { + hxTarget := r.Header.Get("HX-Target") + switch hxTarget { + case "search-results": + return "Results", nil + default: + return "Page", nil + } +} +``` + +This example showcases how structpages can be used to build production-ready applications with minimal boilerplate while maintaining type safety and clean architecture. \ No newline at end of file diff --git a/examples/blog-admin/admin_pages.templ b/examples/blog-admin/admin_pages.templ new file mode 100644 index 0000000..8e786d5 --- /dev/null +++ b/examples/blog-admin/admin_pages.templ @@ -0,0 +1,488 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-playground/form/v4" +) + +// Admin Dashboard +type dashboard struct{} + +type dashboardProps struct { + User *User + TotalPosts int + TotalUsers int + TotalComments int + RecentPosts []*Post + Analytics *Analytics +} + +func (d dashboard) Props(r *http.Request, store *Store, auth *AuthService) (dashboardProps, error) { + user := auth.GetUser(r) + + // Get counts + _, totalPosts, _ := store.ListPosts(PostFilter{Limit: 1}) + _, totalUsers, _ := store.ListUsers(1, 0) + + // Get recent posts + recentPosts, _, _ := store.ListPosts(PostFilter{ + Limit: 5, + Offset: 0, + }) + + // Get today's analytics + analytics, _ := store.GetAnalytics(time.Now()) + + return dashboardProps{ + User: user, + TotalPosts: totalPosts, + TotalUsers: totalUsers, + TotalComments: 0, // Simplified + RecentPosts: recentPosts, + Analytics: analytics, + }, nil +} + +templ (d dashboard) Page(props dashboardProps) { + @adminLayout("Dashboard", props.User) { +

Dashboard

+
+ @statsCard("Total Posts", fmt.Sprint(props.TotalPosts), "+5 this week") + @statsCard("Total Users", fmt.Sprint(props.TotalUsers), "+2 this week") + @statsCard("Page Views Today", fmt.Sprint(props.Analytics.PageViews), "") + @statsCard("Unique Visitors", fmt.Sprint(props.Analytics.Visitors), "") +
+
+
+

Recent Posts

+ @dataTable([]string{"Title", "Author", "Status", "Date", "Actions"}) { + for _, post := range props.RecentPosts { + + { post.Title } + { post.Author.Username } + + + { post.Status } + + + { post.CreatedAt.Format("Jan 2, 2006") } + + Edit + + + } + } +
+
+

Top Posts Today

+ if len(props.Analytics.TopPosts) > 0 { + + } else { +

No data available

+ } +
+
+ } +} + +// Admin Post List +type adminPostListPage struct{} + +type adminPostListProps struct { + User *User + Posts []*Post + Total int + Page int + Filter PostFilter +} + +func (p adminPostListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminPostListProps, error) { + user := auth.GetUser(r) + + // Parse query parameters + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + filter := PostFilter{ + Status: r.URL.Query().Get("status"), + Search: r.URL.Query().Get("search"), + Limit: 20, + Offset: (page - 1) * 20, + } + + // If not admin, only show own posts + if user.Role != "admin" { + filter.AuthorID = user.ID + } + + posts, total, err := store.ListPosts(filter) + if err != nil { + return adminPostListProps{}, err + } + + return adminPostListProps{ + User: user, + Posts: posts, + Total: total, + Page: page, + Filter: filter, + }, nil +} + +templ (p adminPostListPage) Page(props adminPostListProps) { + @adminLayout("Posts", props.User) { + +
+
+ + + +
+
+ @dataTable([]string{"Title", "Author", "Status", "Published", "Views", "Actions"}) { + for _, post := range props.Posts { + + + + { post.Title } + + + { post.Author.Username } + + + { post.Status } + + + + if post.PublishedAt != nil { + { post.PublishedAt.Format("Jan 2, 2006") } + } else { + Not published + } + + { fmt.Sprint(post.ViewCount) } + + Edit + if post.Status == "draft" { + + } + + + + } + } + if baseURL, err := urlFor(ctx, adminPostListPage{}); err == nil { + @pagination(props.Page, (props.Total+19)/20, baseURL) + } + } +} + +// Admin Post Form (New/Edit) +type adminPostNewPage struct{} +type adminPostEditPage struct{} + +type postForm struct { + Title string `form:"title"` + Slug string `form:"slug"` + Content string `form:"content"` + Excerpt string `form:"excerpt"` + Status string `form:"status"` + Categories []string `form:"categories"` + Tags string `form:"tags"` +} + +func (p adminPostNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + user := auth.GetUser(r) + + if r.Method == "POST" { + var f postForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Create post + post := &Post{ + Title: f.Title, + Slug: f.Slug, + Content: f.Content, + Excerpt: f.Excerpt, + Status: f.Status, + AuthorID: user.ID, + } + + if f.Slug == "" { + post.Slug = GenerateSlug(f.Title) + } + + if f.Status == "published" { + now := time.Now() + post.PublishedAt = &now + } + + // Parse tags + if f.Tags != "" { + tags := strings.Split(f.Tags, ",") + for _, tag := range tags { + post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)}) + } + } + + // Add categories + for _, catID := range f.Categories { + if id, err := strconv.Atoi(catID); err == nil { + post.Categories = append(post.Categories, Category{ID: id}) + } + } + + if err := store.CreatePost(post); err != nil { + return err + } + + // Redirect to edit page + http.Redirect(w, r, fmt.Sprintf("/admin/posts/%d/edit", post.ID), http.StatusSeeOther) + return nil + } + + // GET - show form + categories, _ := store.ListCategories() + return render(w, r, postFormPage(user, nil, &postForm{Status: "draft"}, categories, "")) +} + +func (p adminPostEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + user := auth.GetUser(r) + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + if r.Method == "POST" { + var f postForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update post + post.Title = f.Title + post.Slug = f.Slug + post.Content = f.Content + post.Excerpt = f.Excerpt + post.Status = f.Status + + if f.Status == "published" && post.PublishedAt == nil { + now := time.Now() + post.PublishedAt = &now + } + + // Parse tags + post.Tags = nil + if f.Tags != "" { + tags := strings.Split(f.Tags, ",") + for _, tag := range tags { + post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)}) + } + } + + // Update categories + post.Categories = nil + for _, catID := range f.Categories { + if id, err := strconv.Atoi(catID); err == nil { + post.Categories = append(post.Categories, Category{ID: id}) + } + } + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Show success message (simplified - in production use flash messages) + categories, _ := store.ListCategories() + return render(w, r, postFormPage(user, post, &f, categories, "Post updated successfully")) + } + + // GET - show form with existing data + form := &postForm{ + Title: post.Title, + Slug: post.Slug, + Content: post.Content, + Excerpt: post.Excerpt, + Status: post.Status, + } + + // Build tags string + var tags []string + for _, tag := range post.Tags { + tags = append(tags, tag.Name) + } + form.Tags = strings.Join(tags, ", ") + + // Build categories + for _, cat := range post.Categories { + form.Categories = append(form.Categories, fmt.Sprint(cat.ID)) + } + + categories, _ := store.ListCategories() + return render(w, r, postFormPage(user, post, form, categories, "")) +} + +// Post form template +templ postFormPage(user *User, post *Post, form *postForm, categories []*Category, message string) { + @adminLayout(ternary(post == nil, "New Post", "Edit Post"), user) { + + if message != "" { + @alert("success", message) + } +
+
+
+ @formField("Title", "title", "text", form.Title, nil) + @formField("Slug", "slug", "text", form.Slug, nil) + @textareaField("Content", "content", form.Content, 20, nil) + @textareaField("Excerpt", "excerpt", form.Excerpt, 3, nil) +
+ +
+
+ } +} + +// Delete post handler +type adminPostDeletePage struct{} + +func (p adminPostDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + if err := store.DeletePost(postID); err != nil { + return err + } + + // Return empty response for HTMX to remove the row + w.WriteHeader(http.StatusOK) + return nil +} + +// Helper functions +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} diff --git a/examples/blog-admin/admin_pages_templ.go b/examples/blog-admin/admin_pages_templ.go new file mode 100644 index 0000000..17e10fe --- /dev/null +++ b/examples/blog-admin/admin_pages_templ.go @@ -0,0 +1,1038 @@ +// Code generated by templ - DO NOT EDIT. + +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/a-h/templ" + templruntime "github.com/a-h/templ/runtime" + "github.com/go-playground/form/v4" +) + +// Admin Dashboard +type dashboard struct{} + +type dashboardProps struct { + User *User + TotalPosts int + TotalUsers int + TotalComments int + RecentPosts []*Post + Analytics *Analytics +} + +func (d dashboard) Props(r *http.Request, store *Store, auth *AuthService) (dashboardProps, error) { + user := auth.GetUser(r) + + // Get counts + _, totalPosts, _ := store.ListPosts(PostFilter{Limit: 1}) + _, totalUsers, _ := store.ListUsers(1, 0) + + // Get recent posts + recentPosts, _, _ := store.ListPosts(PostFilter{ + Limit: 5, + Offset: 0, + }) + + // Get today's analytics + analytics, _ := store.GetAnalytics(time.Now()) + + return dashboardProps{ + User: user, + TotalPosts: totalPosts, + TotalUsers: totalUsers, + TotalComments: 0, // Simplified + RecentPosts: recentPosts, + Analytics: analytics, + }, nil +} + +func (d dashboard) Page(props dashboardProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Dashboard

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = statsCard("Total Posts", fmt.Sprint(props.TotalPosts), "+5 this week").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = statsCard("Total Users", fmt.Sprint(props.TotalUsers), "+2 this week").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = statsCard("Page Views Today", fmt.Sprint(props.Analytics.PageViews), "").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = statsCard("Unique Visitors", fmt.Sprint(props.Analytics.Visitors), "").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

Recent Posts

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + for _, post := range props.RecentPosts { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 68, Col: 23} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 69, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 = []any{"status", "status-" + post.Status} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(post.Status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 72, Col: 22} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(post.CreatedAt.Format("Jan 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 75, Col: 49} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Edit") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) + templ_7745c5c3_Err = dataTable([]string{"Title", "Author", "Status", "Date", "Actions"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

Top Posts Today

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(props.Analytics.TopPosts) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

No data available

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminLayout("Dashboard", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Admin Post List +type adminPostListPage struct{} + +type adminPostListProps struct { + User *User + Posts []*Post + Total int + Page int + Filter PostFilter +} + +func (p adminPostListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminPostListProps, error) { + user := auth.GetUser(r) + + // Parse query parameters + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + filter := PostFilter{ + Status: r.URL.Query().Get("status"), + Search: r.URL.Query().Get("search"), + Limit: 20, + Offset: (page - 1) * 20, + } + + // If not admin, only show own posts + if user.Role != "admin" { + filter.AuthorID = user.ID + } + + posts, total, err := store.ListPosts(filter) + if err != nil { + return adminPostListProps{}, err + } + + return adminPostListProps{ + User: user, + Posts: posts, + Total: total, + Page: page, + Filter: filter, + }, nil +} + +func (p adminPostListPage) Page(props adminPostListProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Posts

New Post
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var18 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + for _, post := range props.Posts { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 180, Col: 19} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 183, Col: 31} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 = []any{"status", "status-" + post.Status} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(post.Status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 186, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if post.PublishedAt != nil { + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(post.PublishedAt.Format("Jan 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 191, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Not published") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(post.ViewCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_pages.templ`, Line: 196, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Edit ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if post.Status == "draft" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) + templ_7745c5c3_Err = dataTable([]string{"Title", "Author", "Status", "Published", "Views", "Actions"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var18), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if baseURL, err := urlFor(ctx, adminPostListPage{}); err == nil { + templ_7745c5c3_Err = pagination(props.Page, (props.Total+19)/20, baseURL).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) + templ_7745c5c3_Err = adminLayout("Posts", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Admin Post Form (New/Edit) +type adminPostNewPage struct{} +type adminPostEditPage struct{} + +type postForm struct { + Title string `form:"title"` + Slug string `form:"slug"` + Content string `form:"content"` + Excerpt string `form:"excerpt"` + Status string `form:"status"` + Categories []string `form:"categories"` + Tags string `form:"tags"` +} + +func (p adminPostNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + user := auth.GetUser(r) + + if r.Method == "POST" { + var f postForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Create post + post := &Post{ + Title: f.Title, + Slug: f.Slug, + Content: f.Content, + Excerpt: f.Excerpt, + Status: f.Status, + AuthorID: user.ID, + } + + if f.Slug == "" { + post.Slug = GenerateSlug(f.Title) + } + + if f.Status == "published" { + now := time.Now() + post.PublishedAt = &now + } + + // Parse tags + if f.Tags != "" { + tags := strings.Split(f.Tags, ",") + for _, tag := range tags { + post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)}) + } + } + + // Add categories + for _, catID := range f.Categories { + if id, err := strconv.Atoi(catID); err == nil { + post.Categories = append(post.Categories, Category{ID: id}) + } + } + + if err := store.CreatePost(post); err != nil { + return err + } + + // Redirect to edit page + http.Redirect(w, r, fmt.Sprintf("/admin/posts/%d/edit", post.ID), http.StatusSeeOther) + return nil + } + + // GET - show form + categories, _ := store.ListCategories() + return render(w, r, postFormPage(user, nil, &postForm{Status: "draft"}, categories, "")) +} + +func (p adminPostEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + user := auth.GetUser(r) + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + if r.Method == "POST" { + var f postForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update post + post.Title = f.Title + post.Slug = f.Slug + post.Content = f.Content + post.Excerpt = f.Excerpt + post.Status = f.Status + + if f.Status == "published" && post.PublishedAt == nil { + now := time.Now() + post.PublishedAt = &now + } + + // Parse tags + post.Tags = nil + if f.Tags != "" { + tags := strings.Split(f.Tags, ",") + for _, tag := range tags { + post.Tags = append(post.Tags, Tag{Name: strings.TrimSpace(tag)}) + } + } + + // Update categories + post.Categories = nil + for _, catID := range f.Categories { + if id, err := strconv.Atoi(catID); err == nil { + post.Categories = append(post.Categories, Category{ID: id}) + } + } + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Show success message (simplified - in production use flash messages) + categories, _ := store.ListCategories() + return render(w, r, postFormPage(user, post, &f, categories, "Post updated successfully")) + } + + // GET - show form with existing data + form := &postForm{ + Title: post.Title, + Slug: post.Slug, + Content: post.Content, + Excerpt: post.Excerpt, + Status: post.Status, + } + + // Build tags string + var tags []string + for _, tag := range post.Tags { + tags = append(tags, tag.Name) + } + form.Tags = strings.Join(tags, ", ") + + // Build categories + for _, cat := range post.Categories { + form.Categories = append(form.Categories, fmt.Sprint(cat.ID)) + } + + categories, _ := store.ListCategories() + return render(w, r, postFormPage(user, post, form, categories, "")) +} + +// Post form template +func postFormPage(user *User, post *Post, form *postForm, categories []*Category, message string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var30 := templ.GetChildren(ctx) + if templ_7745c5c3_Var30 == nil { + templ_7745c5c3_Var30 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var31 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if post == nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

New Post

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

Edit Post

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if message != "" { + templ_7745c5c3_Err = alert("success", message).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Title", "title", "text", form.Title, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Slug", "slug", "text", form.Slug, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = textareaField("Content", "content", form.Content, 20, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = textareaField("Excerpt", "excerpt", form.Excerpt, 3, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = selectField("Status", "status", form.Status, map[string]string{ + "draft": "Draft", + "published": "Published", + }, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, cat := range categories { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Tags (comma separated)", "tags", "text", form.Tags, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if post != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminLayout(ternary(post == nil, "New Post", "Edit Post"), user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var31), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Delete post handler +type adminPostDeletePage struct{} + +func (p adminPostDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + if err := store.DeletePost(postID); err != nil { + return err + } + + // Return empty response for HTMX to remove the row + w.WriteHeader(http.StatusOK) + return nil +} + +// Helper functions +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +var _ = templruntime.GeneratedTemplate diff --git a/examples/blog-admin/admin_users.templ b/examples/blog-admin/admin_users.templ new file mode 100644 index 0000000..4d40c85 --- /dev/null +++ b/examples/blog-admin/admin_users.templ @@ -0,0 +1,339 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/go-playground/form/v4" +) + +// Admin User List +type adminUserListPage struct{} + +type adminUserListProps struct { + User *User + Users []*User + Total int + Page int +} + +func (u adminUserListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminUserListProps, error) { + user := auth.GetUser(r) + + // Only admins can view users + if user.Role != "admin" { + return adminUserListProps{}, fmt.Errorf("forbidden") + } + + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + limit := 20 + offset := (page - 1) * limit + + users, total, err := store.ListUsers(limit, offset) + if err != nil { + return adminUserListProps{}, err + } + + return adminUserListProps{ + User: user, + Users: users, + Total: total, + Page: page, + }, nil +} + +templ (u adminUserListPage) Page(props adminUserListProps) { + @adminLayout("Users", props.User) { + + @dataTable([]string{"Username", "Email", "Role", "Created", "Actions"}) { + for _, user := range props.Users { + + { user.Username } + { user.Email } + + + { user.Role } + + + { user.CreatedAt.Format("Jan 2, 2006") } + + Edit + if user.ID != props.User.ID { + + } + + + } + } + if baseURL, err := urlFor(ctx, adminUserListPage{}); err == nil { + @pagination(props.Page, (props.Total+19)/20, baseURL) + } + } +} + +// Admin User Form (New/Edit) +type adminUserNewPage struct{} +type adminUserEditPage struct{} + +type userForm struct { + Username string `form:"username"` + Email string `form:"email"` + Password string `form:"password"` + Role string `form:"role"` +} + +func (u adminUserNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + currentUser := auth.GetUser(r) + if currentUser.Role != "admin" { + return fmt.Errorf("forbidden") + } + + if r.Method == "POST" { + var f userForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Hash password + hash, err := HashPassword(f.Password) + if err != nil { + return err + } + + // Create user + user := &User{ + Username: f.Username, + Email: f.Email, + PasswordHash: hash, + Role: f.Role, + } + + if err := store.CreateUser(user); err != nil { + return err + } + + // Redirect to users list + http.Redirect(w, r, "/admin/users", http.StatusSeeOther) + return nil + } + + // GET - show form + return render(w, r, userFormPage(currentUser, nil, &userForm{Role: "reader"}, "")) +} + +func (u adminUserEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + currentUser := auth.GetUser(r) + if currentUser.Role != "admin" { + return fmt.Errorf("forbidden") + } + + userID, _ := strconv.Atoi(r.PathValue("id")) + + user, err := store.GetUserByID(userID) + if err != nil { + return err + } + + if r.Method == "POST" { + var f userForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update user + user.Username = f.Username + user.Email = f.Email + user.Role = f.Role + + // Update password if provided + if f.Password != "" { + hash, err := HashPassword(f.Password) + if err != nil { + return err + } + user.PasswordHash = hash + } + + // In a real app, you'd have an UpdateUser method + // For now, show success + return render(w, r, userFormPage(currentUser, user, &f, "User updated successfully")) + } + + // GET - show form + form := &userForm{ + Username: user.Username, + Email: user.Email, + Role: user.Role, + } + + return render(w, r, userFormPage(currentUser, user, form, "")) +} + +templ userFormPage(currentUser *User, user *User, form *userForm, message string) { + @adminLayout(ternary(user == nil, "New User", "Edit User"), currentUser) { + + if message != "" { + @alert("success", message) + } +
+ @formField("Username", "username", "text", form.Username, nil) + @formField("Email", "email", "email", form.Email, nil) + @formField(ternary(user == nil, "Password", "Password (leave blank to keep current)"), "password", "password", "", nil) + @selectField("Role", "role", form.Role, map[string]string{ + "reader": "Reader", + "author": "Author", + "admin": "Admin", + }, nil) + +
+ } +} + +// Delete user handler +type adminUserDeletePage struct{} + +func (u adminUserDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + currentUser := auth.GetUser(r) + if currentUser.Role != "admin" { + return fmt.Errorf("forbidden") + } + + userID, _ := strconv.Atoi(r.PathValue("id")) + + // Can't delete yourself + if userID == currentUser.ID { + return fmt.Errorf("cannot delete yourself") + } + + // In a real app, you'd have a DeleteUser method + // For now, return success + w.WriteHeader(http.StatusOK) + return nil +} + +// Admin Settings +type adminSettingsPage struct{} + +type settingsForm struct { + SiteName string `form:"site_name"` + SiteDescription string `form:"site_description"` + AdminEmail string `form:"admin_email"` +} + +func (s adminSettingsPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, config *Config, decoder *form.Decoder) error { + user := auth.GetUser(r) + if user.Role != "admin" { + return fmt.Errorf("forbidden") + } + + if r.Method == "POST" { + var f settingsForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update config (in memory only for this example) + config.SiteName = f.SiteName + config.SiteDescription = f.SiteDescription + config.AdminEmail = f.AdminEmail + + // Show success + return render(w, r, s.Page(user, config, "Settings updated successfully")) + } + + // GET - show form + return render(w, r, s.Page(user, config, "")) +} + +templ (s adminSettingsPage) Page(user *User, config *Config, message string) { + @adminLayout("Settings", user) { +

Settings

+ if message != "" { + @alert("success", message) + } +
+ @formField("Site Name", "site_name", "text", config.SiteName, nil) + @formField("Site Description", "site_description", "text", config.SiteDescription, nil) + @formField("Admin Email", "admin_email", "email", config.AdminEmail, nil) + +
+ } +} + +// Media Library +type mediaLibraryPage struct{} + +func (m mediaLibraryPage) Props(r *http.Request, auth *AuthService) (*User, error) { + return auth.GetUser(r), nil +} + +templ (m mediaLibraryPage) Page(user *User) { + @adminLayout("Media Library", user) { + +
+

Media library functionality would go here

+
+ } +} diff --git a/examples/blog-admin/admin_users_templ.go b/examples/blog-admin/admin_users_templ.go new file mode 100644 index 0000000..ed4f14b --- /dev/null +++ b/examples/blog-admin/admin_users_templ.go @@ -0,0 +1,702 @@ +// Code generated by templ - DO NOT EDIT. + +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/a-h/templ" + templruntime "github.com/a-h/templ/runtime" + "github.com/go-playground/form/v4" +) + +// Admin User List +type adminUserListPage struct{} + +type adminUserListProps struct { + User *User + Users []*User + Total int + Page int +} + +func (u adminUserListPage) Props(r *http.Request, store *Store, auth *AuthService) (adminUserListProps, error) { + user := auth.GetUser(r) + + // Only admins can view users + if user.Role != "admin" { + return adminUserListProps{}, fmt.Errorf("forbidden") + } + + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + limit := 20 + offset := (page - 1) * limit + + users, total, err := store.ListUsers(limit, offset) + if err != nil { + return adminUserListProps{}, err + } + + return adminUserListProps{ + User: user, + Users: users, + Total: total, + Page: page, + }, nil +} + +func (u adminUserListPage) Page(props adminUserListProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Users

New User
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + for _, user := range props.Users { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 62, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 63, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 = []any{"role", "role-" + user.Role} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(user.Role) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 66, Col: 18} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.CreatedAt.Format("Jan 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/admin_users.templ`, Line: 69, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Edit ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if user.ID != props.User.ID { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) + templ_7745c5c3_Err = dataTable([]string{"Username", "Email", "Role", "Created", "Actions"}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if baseURL, err := urlFor(ctx, adminUserListPage{}); err == nil { + templ_7745c5c3_Err = pagination(props.Page, (props.Total+19)/20, baseURL).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) + templ_7745c5c3_Err = adminLayout("Users", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Admin User Form (New/Edit) +type adminUserNewPage struct{} +type adminUserEditPage struct{} + +type userForm struct { + Username string `form:"username"` + Email string `form:"email"` + Password string `form:"password"` + Role string `form:"role"` +} + +func (u adminUserNewPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + currentUser := auth.GetUser(r) + if currentUser.Role != "admin" { + return fmt.Errorf("forbidden") + } + + if r.Method == "POST" { + var f userForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Hash password + hash, err := HashPassword(f.Password) + if err != nil { + return err + } + + // Create user + user := &User{ + Username: f.Username, + Email: f.Email, + PasswordHash: hash, + Role: f.Role, + } + + if err := store.CreateUser(user); err != nil { + return err + } + + // Redirect to users list + http.Redirect(w, r, "/admin/users", http.StatusSeeOther) + return nil + } + + // GET - show form + return render(w, r, userFormPage(currentUser, nil, &userForm{Role: "reader"}, "")) +} + +func (u adminUserEditPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + currentUser := auth.GetUser(r) + if currentUser.Role != "admin" { + return fmt.Errorf("forbidden") + } + + userID, _ := strconv.Atoi(r.PathValue("id")) + + user, err := store.GetUserByID(userID) + if err != nil { + return err + } + + if r.Method == "POST" { + var f userForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update user + user.Username = f.Username + user.Email = f.Email + user.Role = f.Role + + // Update password if provided + if f.Password != "" { + hash, err := HashPassword(f.Password) + if err != nil { + return err + } + user.PasswordHash = hash + } + + // In a real app, you'd have an UpdateUser method + // For now, show success + return render(w, r, userFormPage(currentUser, user, &f, "User updated successfully")) + } + + // GET - show form + form := &userForm{ + Username: user.Username, + Email: user.Email, + Role: user.Role, + } + + return render(w, r, userFormPage(currentUser, user, form, "")) +} + +func userFormPage(currentUser *User, user *User, form *userForm, message string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if user == nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

New User

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

Edit User

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if message != "" { + templ_7745c5c3_Err = alert("success", message).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Username", "username", "text", form.Username, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Email", "email", "email", form.Email, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField(ternary(user == nil, "Password", "Password (leave blank to keep current)"), "password", "password", "", nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = selectField("Role", "role", form.Role, map[string]string{ + "reader": "Reader", + "author": "Author", + "admin": "Admin", + }, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminLayout(ternary(user == nil, "New User", "Edit User"), currentUser).Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Delete user handler +type adminUserDeletePage struct{} + +func (u adminUserDeletePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + currentUser := auth.GetUser(r) + if currentUser.Role != "admin" { + return fmt.Errorf("forbidden") + } + + userID, _ := strconv.Atoi(r.PathValue("id")) + + // Can't delete yourself + if userID == currentUser.ID { + return fmt.Errorf("cannot delete yourself") + } + + // In a real app, you'd have a DeleteUser method + // For now, return success + w.WriteHeader(http.StatusOK) + return nil +} + +// Admin Settings +type adminSettingsPage struct{} + +type settingsForm struct { + SiteName string `form:"site_name"` + SiteDescription string `form:"site_description"` + AdminEmail string `form:"admin_email"` +} + +func (s adminSettingsPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, config *Config, decoder *form.Decoder) error { + user := auth.GetUser(r) + if user.Role != "admin" { + return fmt.Errorf("forbidden") + } + + if r.Method == "POST" { + var f settingsForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update config (in memory only for this example) + config.SiteName = f.SiteName + config.SiteDescription = f.SiteDescription + config.AdminEmail = f.AdminEmail + + // Show success + return render(w, r, s.Page(user, config, "Settings updated successfully")) + } + + // GET - show form + return render(w, r, s.Page(user, config, "")) +} + +func (s adminSettingsPage) Page(user *User, config *Config, message string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

Settings

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if message != "" { + templ_7745c5c3_Err = alert("success", message).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Site Name", "site_name", "text", config.SiteName, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Site Description", "site_description", "text", config.SiteDescription, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Admin Email", "admin_email", "email", config.AdminEmail, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminLayout("Settings", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Media Library +type mediaLibraryPage struct{} + +func (m mediaLibraryPage) Props(r *http.Request, auth *AuthService) (*User, error) { + return auth.GetUser(r), nil +} + +func (m mediaLibraryPage) Page(user *User) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var20 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

Media Library

Media library functionality would go here

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = adminLayout("Media Library", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var20), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/examples/blog-admin/api_pages.templ b/examples/blog-admin/api_pages.templ new file mode 100644 index 0000000..7799d84 --- /dev/null +++ b/examples/blog-admin/api_pages.templ @@ -0,0 +1,211 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/go-playground/form/v4" +) + +// API endpoints demonstrating direct HTTP handler pattern + +// Publish post API +type apiPostPublishPage struct{} + +func (p apiPostPublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + // Update status + post.Status = "published" + now := time.Now() + post.PublishedAt = &now + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Return updated row for HTMX + w.Header().Set("Content-Type", "text/html") + return render(w, r, postTableRow(post)) +} + +// Unpublish post API +type apiPostUnpublishPage struct{} + +func (u apiPostUnpublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + // Update status + post.Status = "draft" + post.PublishedAt = nil + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Return updated row for HTMX + w.Header().Set("Content-Type", "text/html") + return render(w, r, postTableRow(post)) +} + +// Auto-save API +type apiPostAutosavePage struct{} + +func (a apiPostAutosavePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + // Parse form data + var f postForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update only content fields (not status) + post.Title = f.Title + post.Content = f.Content + post.Excerpt = f.Excerpt + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Return JSON response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "saved_at": time.Now().Format("3:04 PM"), + }) + return nil +} + +// Media upload API +type apiMediaUploadPage struct{} + +func (m apiMediaUploadPage) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // This demonstrates the standard http.Handler interface (no error return) + // In a real app, you'd handle file uploads here + + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, `
+ Uploaded image + uploaded-file.jpg +
`) +} + +// Template for post table row (used by publish/unpublish APIs) +templ postTableRow(post *Post) { + + + + { post.Title } + + + { post.Author.Username } + + + { post.Status } + + + + if post.PublishedAt != nil { + { post.PublishedAt.Format("Jan 2, 2006") } + } else { + Not published + } + + { fmt.Sprint(post.ViewCount) } + + Edit + if post.Status == "draft" { + + } else { + + } + + + +} + +// Advanced HTMX PageConfig example +func (s searchPage) PageConfig(r *http.Request) (string, error) { + // Custom PageConfig for different HTMX targets + hxTarget := r.Header.Get("HX-Target") + + switch hxTarget { + case "search-results": + // Return just the results component + return "Results", nil + case "search-suggestions": + // Could return a suggestions component + return "Suggestions", nil + default: + // Default to full page + return "Page", nil + } +} + +// Example of Init method +func (m mediaLibraryPage) Init() { + // This would be called during route parsing + // Could be used to set up file storage, etc. + fmt.Println("Initializing media library page") +} diff --git a/examples/blog-admin/api_pages_templ.go b/examples/blog-admin/api_pages_templ.go new file mode 100644 index 0000000..42d6494 --- /dev/null +++ b/examples/blog-admin/api_pages_templ.go @@ -0,0 +1,369 @@ +// Code generated by templ - DO NOT EDIT. + +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/a-h/templ" + templruntime "github.com/a-h/templ/runtime" + "github.com/go-playground/form/v4" +) + +// API endpoints demonstrating direct HTTP handler pattern + +// Publish post API +type apiPostPublishPage struct{} + +func (p apiPostPublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + // Update status + post.Status = "published" + now := time.Now() + post.PublishedAt = &now + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Return updated row for HTMX + w.Header().Set("Content-Type", "text/html") + return render(w, r, postTableRow(post)) +} + +// Unpublish post API +type apiPostUnpublishPage struct{} + +func (u apiPostUnpublishPage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + // Update status + post.Status = "draft" + post.PublishedAt = nil + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Return updated row for HTMX + w.Header().Set("Content-Type", "text/html") + return render(w, r, postTableRow(post)) +} + +// Auto-save API +type apiPostAutosavePage struct{} + +func (a apiPostAutosavePage) ServeHTTP(w http.ResponseWriter, r *http.Request, store *Store, auth *AuthService, decoder *form.Decoder) error { + postID, _ := strconv.Atoi(r.PathValue("id")) + + post, err := store.GetPostByID(postID) + if err != nil { + return err + } + + // Check permission + user := auth.GetUser(r) + if user.Role != "admin" && post.AuthorID != user.ID { + return fmt.Errorf("forbidden") + } + + // Parse form data + var f postForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + // Update only content fields (not status) + post.Title = f.Title + post.Content = f.Content + post.Excerpt = f.Excerpt + + if err := store.UpdatePost(post); err != nil { + return err + } + + // Return JSON response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "saved_at": time.Now().Format("3:04 PM"), + }) + return nil +} + +// Media upload API +type apiMediaUploadPage struct{} + +func (m apiMediaUploadPage) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // This demonstrates the standard http.Handler interface (no error return) + // In a real app, you'd handle file uploads here + + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, `
+ Uploaded image + uploaded-file.jpg +
`) +} + +// Template for post table row (used by publish/unpublish APIs) +func postTableRow(post *Post) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 139, Col: 16} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 142, Col: 28} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 = []any{"status", "status-" + post.Status} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(post.Status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 145, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if post.PublishedAt != nil { + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(post.PublishedAt.Format("Jan 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 150, Col: 44} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Not published") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(post.ViewCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/api_pages.templ`, Line: 155, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Edit ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if post.Status == "draft" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Advanced HTMX PageConfig example +func (s searchPage) PageConfig(r *http.Request) (string, error) { + // Custom PageConfig for different HTMX targets + hxTarget := r.Header.Get("HX-Target") + + switch hxTarget { + case "search-results": + // Return just the results component + return "Results", nil + case "search-suggestions": + // Could return a suggestions component + return "Suggestions", nil + default: + // Default to full page + return "Page", nil + } +} + +// Example of Init method +func (m mediaLibraryPage) Init() { + // This would be called during route parsing + // Could be used to set up file storage, etc. + fmt.Println("Initializing media library page") +} + +var _ = templruntime.GeneratedTemplate diff --git a/examples/blog-admin/auth.go b/examples/blog-admin/auth.go new file mode 100644 index 0000000..66d3997 --- /dev/null +++ b/examples/blog-admin/auth.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "fmt" + "net/http" + + "github.com/alexedwards/scs/v2" + "golang.org/x/crypto/bcrypt" +) + +// AuthService handles authentication +type AuthService struct { + store *Store + session *scs.SessionManager +} + +func NewAuthService(store *Store, session *scs.SessionManager) *AuthService { + return &AuthService{ + store: store, + session: session, + } +} + +// Login authenticates a user +func (a *AuthService) Login(r *http.Request, username, password string) (*User, error) { + user, err := a.store.GetUserByUsername(username) + if err != nil { + return nil, fmt.Errorf("invalid credentials") + } + + // Check password + err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) + if err != nil { + return nil, fmt.Errorf("invalid credentials") + } + + // Store user ID in session + a.session.Put(r.Context(), "userID", user.ID) + + return user, nil +} + +// Logout logs out the current user +func (a *AuthService) Logout(r *http.Request) { + a.session.Remove(r.Context(), "userID") +} + +// GetUser returns the currently logged in user +func (a *AuthService) GetUser(r *http.Request) *User { + userID := a.session.GetInt(r.Context(), "userID") + if userID == 0 { + return nil + } + + user, err := a.store.GetUserByID(userID) + if err != nil { + return nil + } + + return user +} + +// IsAuthenticated checks if a user is logged in +func (a *AuthService) IsAuthenticated(r *http.Request) bool { + return a.GetUser(r) != nil +} + +// IsAdmin checks if the current user is an admin +func (a *AuthService) IsAdmin(r *http.Request) bool { + user := a.GetUser(r) + return user != nil && user.Role == "admin" +} + +// IsAuthor checks if the current user is an author or admin +func (a *AuthService) IsAuthor(r *http.Request) bool { + user := a.GetUser(r) + return user != nil && (user.Role == "author" || user.Role == "admin") +} + +// HashPassword creates a bcrypt hash of the password +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// Context keys for passing auth data +type contextKey string + +const ( + userContextKey contextKey = "user" +) + +// WithUser adds user to context +func WithUser(ctx context.Context, user *User) context.Context { + return context.WithValue(ctx, userContextKey, user) +} + +// UserFromContext retrieves user from context +func UserFromContext(ctx context.Context) *User { + user, ok := ctx.Value(userContextKey).(*User) + if !ok { + return nil + } + return user +} diff --git a/examples/blog-admin/components.templ b/examples/blog-admin/components.templ new file mode 100644 index 0000000..e9caa51 --- /dev/null +++ b/examples/blog-admin/components.templ @@ -0,0 +1,272 @@ +package main + +import ( + "context" + "fmt" + "github.com/jackielii/structpages" +) + +// URL generation helpers +func urlFor(ctx context.Context, page any, args ...any) (templ.SafeURL, error) { + url, err := structpages.URLFor(ctx, page, args...) + return templ.URL(url), err +} + +func join(page any, pattern string) string { + // Simplified join for query parameters + return fmt.Sprintf("%T%s", page, pattern) +} + +// Layout components +templ layout(title string, user *User) { + + + + + + { title } + + + + + + @navbar(user) +
+ { children... } +
+ @footer() + + +} + +templ adminLayout(title string, user *User) { + + + + + + { title } - Admin + + + + + + @adminNavbar(user) +
+ @adminSidebar() +
+ { children... } +
+
+ + +} + +// Navigation components +templ navbar(user *User) { + +} + +templ adminNavbar(user *User) { + +} + +templ adminSidebar() { + +} + +templ footer() { + +} + +// UI Components +templ card(title string) { +
+ if title != "" { +
+

{ title }

+
+ } +
+ { children... } +
+
+} + +templ alert(variant string, message string) { + +} + +templ formField(label, name, inputType string, value string, errors []string) { +
+ + 0) } + /> + for _, err := range errors { + { err } + } +
+} + +templ textareaField(label, name string, value string, rows int, errors []string) { +
+ + + for _, err := range errors { + { err } + } +
+} + +templ selectField(label, name string, value string, options map[string]string, errors []string) { +
+ + + for _, err := range errors { + { err } + } +
+} + +templ pagination(currentPage, totalPages int, baseURL templ.SafeURL) { + if totalPages > 1 { + + } +} + +templ statsCard(title, value, change string) { +
+

{ title }

+
{ value }
+ if change != "" { +
{ change }
+ } +
+} + +templ dataTable(headers []string) { + + + + for _, header := range headers { + + } + + + + { children... } + +
{ header }
+} + +templ confirmModal(id, title, message, action string) { + +} + +// Loading states +templ loading() { +
+
+ Loading... +
+} + +templ htmxIndicator() { +
+
+
+} diff --git a/examples/blog-admin/components_templ.go b/examples/blog-admin/components_templ.go new file mode 100644 index 0000000..ad43d2e --- /dev/null +++ b/examples/blog-admin/components_templ.go @@ -0,0 +1,1532 @@ +// Code generated by templ - DO NOT EDIT. + +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "context" + "fmt" + + "github.com/a-h/templ" + templruntime "github.com/a-h/templ/runtime" + "github.com/jackielii/structpages" +) + +// URL generation helpers +func urlFor(ctx context.Context, page any, args ...any) (templ.SafeURL, error) { + url, err := structpages.URLFor(ctx, page, args...) + return templ.URL(url), err +} + +func join(page any, pattern string) string { + // Simplified join for query parameters + return fmt.Sprintf("%T%s", page, pattern) +} + +// Layout components +func layout(title string, user *User) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 28, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = navbar(user).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func adminLayout(title string, user *User) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 49, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " - Admin") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = adminNavbar(user).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = adminSidebar().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var3.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Navigation components +func navbar(user *User) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func adminNavbar(user *User) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func adminSidebar() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func footer() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// UI Components +func card(title string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 133, Col: 15} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var26.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func alert(variant string, message string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var28 := templ.GetChildren(ctx) + if templ_7745c5c3_Var28 == nil { + templ_7745c5c3_Var28 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var29 = []any{"alert", "alert-" + variant} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 144, Col: 11} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func formField(label, name, inputType string, value string, errors []string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var32 := templ.GetChildren(ctx) + if templ_7745c5c3_Var32 == nil { + templ_7745c5c3_Var32 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var35 = []any{templ.KV("error", len(errors) > 0)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var35...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, err := range errors { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(err) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 159, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func textareaField(label, name string, value string, rows int, errors []string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var42 := templ.GetChildren(ctx) + if templ_7745c5c3_Var42 == nil { + templ_7745c5c3_Var42 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var45 = []any{templ.KV("error", len(errors) > 0)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var45...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, err := range errors { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var51 string + templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(err) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 174, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func selectField(label, name string, value string, options map[string]string, errors []string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var52 := templ.GetChildren(ctx) + if templ_7745c5c3_Var52 == nil { + templ_7745c5c3_Var52 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var55 = []any{templ.KV("error", len(errors) > 0)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var55...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, err := range errors { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var61 string + templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(err) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 192, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func pagination(currentPage, totalPages int, baseURL templ.SafeURL) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var62 := templ.GetChildren(ctx) + if templ_7745c5c3_Var62 == nil { + templ_7745c5c3_Var62 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if totalPages > 1 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func statsCard(title, value, change string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var68 := templ.GetChildren(ctx) + if templ_7745c5c3_Var68 == nil { + templ_7745c5c3_Var68 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var69 string + templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 221, Col: 13} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var70 string + templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(value) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 222, Col: 28} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if change != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var71 string + templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(change) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 224, Col: 31} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func dataTable(headers []string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var72 := templ.GetChildren(ctx) + if templ_7745c5c3_Var72 == nil { + templ_7745c5c3_Var72 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, header := range headers { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var72.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var73 string + templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(header) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 234, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func confirmModal(id, title, message, action string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var74 := templ.GetChildren(ctx) + if templ_7745c5c3_Var74 == nil { + templ_7745c5c3_Var74 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var76 string + templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 248, Col: 14} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var77 string + templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/components.templ`, Line: 249, Col: 15} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Loading states +func loading() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var80 := templ.GetChildren(ctx) + if templ_7745c5c3_Var80 == nil { + templ_7745c5c3_Var80 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "
Loading...
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func htmxIndicator() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var81 := templ.GetChildren(ctx) + if templ_7745c5c3_Var81 == nil { + templ_7745c5c3_Var81 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/examples/blog-admin/errors.go b/examples/blog-admin/errors.go new file mode 100644 index 0000000..bbd4e9e --- /dev/null +++ b/examples/blog-admin/errors.go @@ -0,0 +1,18 @@ +package main + +import "fmt" + +// HTTPError represents an HTTP error with a status code +type HTTPError struct { + Code int + Message string +} + +func (e HTTPError) Error() string { + return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message) +} + +// NewHTTPError creates a new HTTP error +func NewHTTPError(code int, message string) HTTPError { + return HTTPError{Code: code, Message: message} +} diff --git a/examples/blog-admin/go.mod b/examples/blog-admin/go.mod new file mode 100644 index 0000000..4835737 --- /dev/null +++ b/examples/blog-admin/go.mod @@ -0,0 +1,20 @@ +module github.com/jackielii/structpages/examples/blog-admin + +go 1.24.0 + +toolchain go1.24.3 + +replace github.com/jackielii/structpages => ../.. + +require ( + github.com/alexedwards/scs/v2 v2.8.0 + github.com/go-playground/form/v4 v4.2.1 + github.com/jackielii/structpages v0.0.0-00010101000000-000000000000 + github.com/mattn/go-sqlite3 v1.14.24 + golang.org/x/crypto v0.31.0 +) + +require ( + github.com/a-h/templ v0.3.898 // indirect + github.com/jackielii/ctxkey v1.0.1 // indirect +) diff --git a/examples/blog-admin/go.sum b/examples/blog-admin/go.sum new file mode 100644 index 0000000..64101a6 --- /dev/null +++ b/examples/blog-admin/go.sum @@ -0,0 +1,16 @@ +github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s= +github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= +github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= +github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw= +github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/jackielii/ctxkey v1.0.1 h1:CcgbR+fQbrzZJWxI/7Ec4EhzUbmTU1sfI1gV7MAgjIg= +github.com/jackielii/ctxkey v1.0.1/go.mod h1:fo4HOwrvSnc3n8o5qZ5L+FVcSyQn+d67CCnlEbH24uc= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= diff --git a/examples/blog-admin/main.go b/examples/blog-admin/main.go new file mode 100644 index 0000000..3837cc4 --- /dev/null +++ b/examples/blog-admin/main.go @@ -0,0 +1,196 @@ +package main + +import ( + "database/sql" + "embed" + "errors" + "flag" + "fmt" + "log" + "net/http" + "time" + + "github.com/alexedwards/scs/v2" + "github.com/alexedwards/scs/v2/memstore" + "github.com/go-playground/form/v4" + "github.com/jackielii/structpages" + _ "github.com/mattn/go-sqlite3" +) + +//go:embed static/* +var staticFS embed.FS + +var ( + flagPort = flag.String("port", "8080", "Port to listen on") + flagDB = flag.String("db", "blog.db", "Database file path") +) + +func main() { + flag.Parse() + + // Initialize database + db, err := initDB(*flagDB) + if err != nil { + log.Fatal("Failed to initialize database:", err) + } + defer db.Close() + + // Initialize services + sessionManager := initSessionManager() + store := NewStore(db) + auth := NewAuthService(store, sessionManager) + formDecoder := form.NewDecoder() + formDecoder.SetTagName("form") + config := &Config{ + SiteName: "Structpages Blog", + SiteDescription: "A powerful blog platform built with structpages", + AdminEmail: "admin@example.com", + } + + // Initialize structpages + sp := structpages.New( + structpages.WithDefaultPageConfig(structpages.HTMXPageConfig), + structpages.WithErrorHandler(customErrorHandler), + structpages.WithMiddlewares( + wrapMiddleware(sessionMiddleware(sessionManager)), + wrapMiddleware(loggingMiddleware), + ), + ) + + // Create router + mux := http.NewServeMux() + r := structpages.NewRouter(mux) + + // Mount pages with dependency injection + if err := sp.MountPages(r, pages{}, "/", "Structpages Blog", + store, + auth, + sessionManager, + formDecoder, + config, + ); err != nil { + log.Fatal("Failed to mount pages:", err) + } + + // Serve static files + fileServer := http.FileServer(http.FS(staticFS)) + mux.Handle("/static/", fileServer) + + // Print available routes + fmt.Println("\nAvailable routes:") + printRoutes(mux, "") + + // Start server + addr := fmt.Sprintf(":%s", *flagPort) + log.Printf("Starting blog server on http://localhost%s", addr) + if err := http.ListenAndServe(addr, r); err != nil { + log.Fatal(err) + } +} + +func initDB(dbPath string) (*sql.DB, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, err + } + + // Enable foreign keys + if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { + return nil, err + } + + // Create tables + if err := createTables(db); err != nil { + return nil, err + } + + return db, nil +} + +func initSessionManager() *scs.SessionManager { + sessionManager := scs.New() + sessionManager.Store = memstore.New() + sessionManager.Lifetime = 24 * time.Hour + sessionManager.Cookie.Name = "blog_session" + sessionManager.Cookie.HttpOnly = true + sessionManager.Cookie.Persist = true + sessionManager.Cookie.SameSite = http.SameSiteLaxMode + sessionManager.Cookie.Secure = false // Set to true in production with HTTPS + return sessionManager +} + +func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + log.Printf("Error handling request %s: %v", r.URL.Path, err) + + // Check if it's an HTTP error + var httpErr HTTPError + if errors.As(err, &httpErr) { + w.WriteHeader(httpErr.Code) + if httpErr.Code == http.StatusNotFound { + fmt.Fprintf(w, "Page not found: %s", r.URL.Path) + } else { + fmt.Fprintf(w, "Error %d: %s", httpErr.Code, httpErr.Message) + } + return + } + + // Default error response + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Internal server error") +} + +// Middleware + +func sessionMiddleware(sm *scs.SessionManager) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return sm.LoadAndSave(next) + } +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start)) + }) +} + +// wrapMiddleware converts a standard middleware to structpages MiddlewareFunc +func wrapMiddleware(mw func(http.Handler) http.Handler) structpages.MiddlewareFunc { + return func(next http.Handler, pn *structpages.PageNode) http.Handler { + return mw(next) + } +} + +// Print routes helper +func printRoutes(mux *http.ServeMux, prefix string) { + // This is a simplified version - in production you'd use reflection + // or a more sophisticated approach to list all routes + routes := []string{ + "/", + "/posts/{slug}", + "/category/{slug}", + "/search", + "/login", + "/logout", + "/admin", + "/admin/posts", + "/admin/posts/new", + "/admin/posts/{id}/edit", + "/admin/users", + "/admin/settings", + "/api/posts/{id}/publish", + "/static/", + } + + for _, route := range routes { + fmt.Printf("%s%s\n", prefix, route) + } +} + +// Config holds application configuration +type Config struct { + SiteName string + SiteDescription string + AdminEmail string +} diff --git a/examples/blog-admin/models.go b/examples/blog-admin/models.go new file mode 100644 index 0000000..bdfa1e5 --- /dev/null +++ b/examples/blog-admin/models.go @@ -0,0 +1,224 @@ +package main + +import ( + "database/sql" + "time" +) + +// User represents a blog user +type User struct { + ID int + Username string + Email string + PasswordHash string + Role string // "admin", "author", "reader" + CreatedAt time.Time + UpdatedAt time.Time +} + +// Post represents a blog post +type Post struct { + ID int + Slug string + Title string + Content string + Excerpt string + AuthorID int + Author *User + Status string // "draft", "published" + PublishedAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time + ViewCount int + Tags []Tag + Categories []Category +} + +// Category represents a blog category +type Category struct { + ID int + Slug string + Name string + Description string + PostCount int + CreatedAt time.Time +} + +// Tag represents a blog tag +type Tag struct { + ID int + Name string + PostCount int +} + +// Comment represents a blog comment +type Comment struct { + ID int + PostID int + Post *Post + AuthorID *int + Author *User + Name string // For anonymous comments + Email string // For anonymous comments + Content string + Status string // "pending", "approved", "spam" + CreatedAt time.Time +} + +// Media represents uploaded media files +type Media struct { + ID int + Filename string + Path string + MimeType string + Size int64 + UploadedBy int + UploadedAt time.Time + Alt string + Description string +} + +// Analytics represents basic analytics data +type Analytics struct { + Date time.Time + PageViews int + Visitors int + NewUsers int + TopPosts []PostAnalytics +} + +type PostAnalytics struct { + PostID int + PostTitle string + Views int +} + +// Database schema +func createTables(db *sql.DB) error { + schema := ` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'reader', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + excerpt TEXT, + author_id INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'draft', + published_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + view_count INTEGER DEFAULT 0, + FOREIGN KEY (author_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL + ); + + CREATE TABLE IF NOT EXISTS post_categories ( + post_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + PRIMARY KEY (post_id, category_id), + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS post_tags ( + post_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (post_id, tag_id), + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + post_id INTEGER NOT NULL, + author_id INTEGER, + name TEXT, + email TEXT, + content TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL + ); + + CREATE TABLE IF NOT EXISTS media ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + path TEXT NOT NULL, + mime_type TEXT NOT NULL, + size INTEGER NOT NULL, + uploaded_by INTEGER NOT NULL, + uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, + alt TEXT, + description TEXT, + FOREIGN KEY (uploaded_by) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS page_views ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + post_id INTEGER, + path TEXT NOT NULL, + ip_hash TEXT NOT NULL, + user_agent TEXT, + referrer TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE + ); + + -- Create indexes + CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug); + CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status); + CREATE INDEX IF NOT EXISTS idx_posts_published_at ON posts(published_at); + CREATE INDEX IF NOT EXISTS idx_categories_slug ON categories(slug); + CREATE INDEX IF NOT EXISTS idx_comments_post_id ON comments(post_id); + CREATE INDEX IF NOT EXISTS idx_comments_status ON comments(status); + CREATE INDEX IF NOT EXISTS idx_page_views_created_at ON page_views(created_at); + + -- Insert default admin user (password: admin123) + INSERT OR IGNORE INTO users (id, username, email, password_hash, role) + VALUES (1, 'admin', 'admin@example.com', '$2a$10$i/CMm4KCafFhALzoVAqRxO8MpyllRakjq5gwOFH4KMdeIWodOjKyS', 'admin'); + + -- Insert sample categories + INSERT OR IGNORE INTO categories (id, slug, name, description) VALUES + (1, 'technology', 'Technology', 'Posts about technology and programming'), + (2, 'design', 'Design', 'Posts about design and UX'), + (3, 'business', 'Business', 'Posts about business and entrepreneurship'); + + -- Insert sample posts + INSERT OR IGNORE INTO posts (id, slug, title, content, excerpt, author_id, status, published_at, view_count) VALUES + (1, 'welcome-to-structpages', 'Welcome to Structpages Blog', '

This is a demo blog built with the structpages framework. It demonstrates advanced features like nested routing, authentication, and HTMX integration.

', 'A demo blog showcasing structpages features', 1, 'published', CURRENT_TIMESTAMP, 42), + (2, 'building-with-go', 'Building Web Apps with Go', '

Go is an excellent choice for building web applications. Its simplicity and performance make it ideal for modern web development.

', 'Why Go is great for web development', 1, 'published', CURRENT_TIMESTAMP, 15), + (3, 'draft-post', 'Work in Progress', '

This is a draft post that is not yet published.

', 'Coming soon...', 1, 'draft', NULL, 0); + + -- Link posts to categories + INSERT OR IGNORE INTO post_categories (post_id, category_id) VALUES + (1, 1), (1, 2), + (2, 1), + (3, 3); + ` + + _, err := db.Exec(schema) + return err +} diff --git a/examples/blog-admin/pages.templ b/examples/blog-admin/pages.templ new file mode 100644 index 0000000..3c79b64 --- /dev/null +++ b/examples/blog-admin/pages.templ @@ -0,0 +1,475 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/go-playground/form/v4" +) + +// Home page +type homePage struct{} + +type homePageProps struct { + RecentPosts []*Post + PopularPosts []*Post + Categories []*Category + User *User +} + +func (h homePage) Props(r *http.Request, store *Store, auth *AuthService) (homePageProps, error) { + // Get recent posts + recentPosts, _, err := store.ListPosts(PostFilter{ + Status: "published", + Limit: 5, + Offset: 0, + }) + if err != nil { + return homePageProps{}, err + } + + // Get popular posts (simplified - just get posts with high view count) + popularPosts, _, err := store.ListPosts(PostFilter{ + Status: "published", + Limit: 5, + Offset: 0, + }) + if err != nil { + return homePageProps{}, err + } + + // Get categories + categories, err := store.ListCategories() + if err != nil { + return homePageProps{}, err + } + + return homePageProps{ + RecentPosts: recentPosts, + PopularPosts: popularPosts, + Categories: categories, + User: auth.GetUser(r), + }, nil +} + +templ (h homePage) Page(props homePageProps) { + @layout("Home", props.User) { +
+
+

Welcome to Structpages Blog

+

A powerful blog platform demonstrating advanced structpages features

+
+
+
+

Recent Posts

+ for _, post := range props.RecentPosts { + @postCard(post) + } +
+ +
+
+ } +} + +// Post card component +templ postCard(post *Post) { +
+

+ { post.Title } +

+
+ By { post.Author.Username } + { post.CreatedAt.Format("Jan 2, 2006") } +
+

{ post.Excerpt }

+ Read more → +
+} + +// Single post page +type postPage struct{} + +type postPageProps struct { + Post *Post + RelatedPosts []*Post + User *User +} + +func (p postPage) Props(r *http.Request, store *Store, auth *AuthService) (postPageProps, error) { + slug := r.PathValue("slug") + + post, err := store.GetPostBySlug(slug) + if err != nil { + return postPageProps{}, err + } + + // Only show published posts to non-authors + user := auth.GetUser(r) + if post.Status != "published" && (user == nil || (user.ID != post.AuthorID && user.Role != "admin")) { + return postPageProps{}, fmt.Errorf("post not found") + } + + // Increment view count + go store.IncrementPostViews(post.ID) + + // Get related posts (simplified - just get posts from same category) + var relatedPosts []*Post + if len(post.Categories) > 0 { + related, _, _ := store.ListPosts(PostFilter{ + Status: "published", + CategoryID: post.Categories[0].ID, + Limit: 3, + Offset: 0, + }) + for _, p := range related { + if p.ID != post.ID { + relatedPosts = append(relatedPosts, p) + } + } + } + + return postPageProps{ + Post: post, + RelatedPosts: relatedPosts, + User: user, + }, nil +} + +templ (p postPage) Page(props postPageProps) { + @layout(props.Post.Title, props.User) { +
+
+

{ props.Post.Title }

+ + if props.Post.Status == "draft" { + @alert("warning", "This post is a draft and not yet published") + } +
+
+ @templ.Raw(props.Post.Content) +
+ + if len(props.RelatedPosts) > 0 { + + } +
+ } +} + +// Category page +type categoryPage struct{} + +type categoryPageProps struct { + Category *Category + Posts []*Post + Total int + Page int + User *User +} + +func (c categoryPage) Props(r *http.Request, store *Store, auth *AuthService) (categoryPageProps, error) { + slug := r.PathValue("slug") + + category, err := store.GetCategoryBySlug(slug) + if err != nil { + return categoryPageProps{}, err + } + + // Get page number from query + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + limit := 10 + offset := (page - 1) * limit + + posts, total, err := store.ListPosts(PostFilter{ + Status: "published", + CategoryID: category.ID, + Limit: limit, + Offset: offset, + }) + if err != nil { + return categoryPageProps{}, err + } + + return categoryPageProps{ + Category: category, + Posts: posts, + Total: total, + Page: page, + User: auth.GetUser(r), + }, nil +} + +templ (c categoryPage) Page(props categoryPageProps) { + @layout(props.Category.Name, props.User) { +
+
+

{ props.Category.Name }

+ if props.Category.Description != "" { +

{ props.Category.Description }

+ } + { fmt.Sprintf("%d posts", props.Total) } +
+
+ for _, post := range props.Posts { + @postCard(post) + } +
+ if url, err := urlFor(ctx, categoryPage{}, props.Category.Slug); err == nil { + @pagination(props.Page, (props.Total+9)/10, url) + } +
+ } +} + +// Search page +type searchPage struct{} + +type searchPageProps struct { + Query string + Results []*Post + Total int + Page int + User *User +} + +func (s searchPage) Props(r *http.Request, store *Store, auth *AuthService) (searchPageProps, error) { + query := r.URL.Query().Get("q") + + // Get page number from query + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + var results []*Post + var total int + + if query != "" { + limit := 10 + offset := (page - 1) * limit + + var err error + results, total, err = store.ListPosts(PostFilter{ + Status: "published", + Search: query, + Limit: limit, + Offset: offset, + }) + if err != nil { + return searchPageProps{}, err + } + } + + return searchPageProps{ + Query: query, + Results: results, + Total: total, + Page: page, + User: auth.GetUser(r), + }, nil +} + +templ (s searchPage) Page(props searchPageProps) { + @layout("Search", props.User) { +
+

Search

+
+ + +
+ if props.Query != "" { +
+

+ { fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query) } +

+ if len(props.Results) > 0 { +
+ for _, post := range props.Results { + @postCard(post) + } +
+ if baseURL, err := urlFor(ctx, searchPage{}); err == nil { + @pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query))) + } + } else { +

No posts found matching your search.

+ } +
+ } +
+ } +} + +// HTMX partial rendering for search +templ (s searchPage) Results(props searchPageProps) { +
+

+ { fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query) } +

+ if len(props.Results) > 0 { +
+ for _, post := range props.Results { + @postCard(post) + } +
+ if baseURL, err := urlFor(ctx, searchPage{}); err == nil { + @pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query))) + } + } else { +

No posts found matching your search.

+ } +
+} + +// Login page +type loginPage struct{} + +type loginForm struct { + Username string `form:"username"` + Password string `form:"password"` + Redirect string `form:"redirect"` +} + +func (l loginPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, decoder *form.Decoder) error { + // Already logged in? + if auth.IsAuthenticated(r) { + http.Redirect(w, r, "/admin", http.StatusSeeOther) + return nil + } + + if r.Method == "POST" { + var f loginForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + _, err := auth.Login(r, f.Username, f.Password) + if err != nil { + // Show error + return render(w, r, l.Page(auth.GetUser(r), &f, err.Error())) + } + + // Redirect to admin or requested page + redirect := f.Redirect + if redirect == "" { + redirect = "/admin" + } + http.Redirect(w, r, redirect, http.StatusSeeOther) + return nil + } + + // GET request - show form + redirect := r.URL.Query().Get("redirect") + return render(w, r, l.Page(auth.GetUser(r), &loginForm{Redirect: redirect}, "")) +} + +templ (l loginPage) Page(user *User, form *loginForm, errorMsg string) { + @layout("Login", user) { +
+
+

Login

+ if errorMsg != "" { + @alert("error", errorMsg) + } +
+ if form.Redirect != "" { + + } + @formField("Username", "username", "text", form.Username, nil) + @formField("Password", "password", "password", "", nil) + +
+

Demo credentials: admin / admin123

+
+
+ } +} + +// Logout page +type logoutPage struct{} + +func (l logoutPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService) error { + auth.Logout(r) + http.Redirect(w, r, "/", http.StatusSeeOther) + return nil +} diff --git a/examples/blog-admin/pages_templ.go b/examples/blog-admin/pages_templ.go new file mode 100644 index 0000000..60711e7 --- /dev/null +++ b/examples/blog-admin/pages_templ.go @@ -0,0 +1,1182 @@ +// Code generated by templ - DO NOT EDIT. + +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/a-h/templ" + templruntime "github.com/a-h/templ/runtime" + "github.com/go-playground/form/v4" +) + +// Home page +type homePage struct{} + +type homePageProps struct { + RecentPosts []*Post + PopularPosts []*Post + Categories []*Category + User *User +} + +func (h homePage) Props(r *http.Request, store *Store, auth *AuthService) (homePageProps, error) { + // Get recent posts + recentPosts, _, err := store.ListPosts(PostFilter{ + Status: "published", + Limit: 5, + Offset: 0, + }) + if err != nil { + return homePageProps{}, err + } + + // Get popular posts (simplified - just get posts with high view count) + popularPosts, _, err := store.ListPosts(PostFilter{ + Status: "published", + Limit: 5, + Offset: 0, + }) + if err != nil { + return homePageProps{}, err + } + + // Get categories + categories, err := store.ListCategories() + if err != nil { + return homePageProps{}, err + } + + return homePageProps{ + RecentPosts: recentPosts, + PopularPosts: popularPosts, + Categories: categories, + User: auth.GetUser(r), + }, nil +} + +func (h homePage) Page(props homePageProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Welcome to Structpages Blog

A powerful blog platform demonstrating advanced structpages features

Recent Posts

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, post := range props.RecentPosts { + templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = layout("Home", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Post card component +func postCard(post *Post) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 110, Col: 62} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

By ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(post.Author.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 113, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(post.CreatedAt.Format("Jan 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 114, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(post.Excerpt) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 116, Col: 19} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "

Read more →
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Single post page +type postPage struct{} + +type postPageProps struct { + Post *Post + RelatedPosts []*Post + User *User +} + +func (p postPage) Props(r *http.Request, store *Store, auth *AuthService) (postPageProps, error) { + slug := r.PathValue("slug") + + post, err := store.GetPostBySlug(slug) + if err != nil { + return postPageProps{}, err + } + + // Only show published posts to non-authors + user := auth.GetUser(r) + if post.Status != "published" && (user == nil || (user.ID != post.AuthorID && user.Role != "admin")) { + return postPageProps{}, fmt.Errorf("post not found") + } + + // Increment view count + go store.IncrementPostViews(post.ID) + + // Get related posts (simplified - just get posts from same category) + var relatedPosts []*Post + if len(post.Categories) > 0 { + related, _, _ := store.ListPosts(PostFilter{ + Status: "published", + CategoryID: post.Categories[0].ID, + Limit: 3, + Offset: 0, + }) + for _, p := range related { + if p.ID != post.ID { + relatedPosts = append(relatedPosts, p) + } + } + } + + return postPageProps{ + Post: post, + RelatedPosts: relatedPosts, + User: user, + }, nil +} + +func (p postPage) Page(props postPageProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(props.Post.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 174, Col: 26} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

By ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(props.Post.Author.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 176, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(props.Post.CreatedAt.Format("January 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 177, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d views", props.Post.ViewCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 178, Col: 58} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.Post.Status == "draft" { + templ_7745c5c3_Err = alert("warning", "This post is a draft and not yet published").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.Raw(props.Post.Content).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(props.RelatedPosts) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

Related Posts

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, post := range props.RelatedPosts { + templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = layout(props.Post.Title, props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Category page +type categoryPage struct{} + +type categoryPageProps struct { + Category *Category + Posts []*Post + Total int + Page int + User *User +} + +func (c categoryPage) Props(r *http.Request, store *Store, auth *AuthService) (categoryPageProps, error) { + slug := r.PathValue("slug") + + category, err := store.GetCategoryBySlug(slug) + if err != nil { + return categoryPageProps{}, err + } + + // Get page number from query + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + limit := 10 + offset := (page - 1) * limit + + posts, total, err := store.ListPosts(PostFilter{ + Status: "published", + CategoryID: category.ID, + Limit: limit, + Offset: offset, + }) + if err != nil { + return categoryPageProps{}, err + } + + return categoryPageProps{ + Category: category, + Posts: posts, + Total: total, + Page: page, + User: auth.GetUser(r), + }, nil +} + +func (c categoryPage) Page(props categoryPageProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var26 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(props.Category.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 282, Col: 29} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.Category.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(props.Category.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 284, Col: 36} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d posts", props.Total)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 286, Col: 67} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, post := range props.Posts { + templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if url, err := urlFor(ctx, categoryPage{}, props.Category.Slug); err == nil { + templ_7745c5c3_Err = pagination(props.Page, (props.Total+9)/10, url).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = layout(props.Category.Name, props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var26), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Search page +type searchPage struct{} + +type searchPageProps struct { + Query string + Results []*Post + Total int + Page int + User *User +} + +func (s searchPage) Props(r *http.Request, store *Store, auth *AuthService) (searchPageProps, error) { + query := r.URL.Query().Get("q") + + // Get page number from query + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } + + var results []*Post + var total int + + if query != "" { + limit := 10 + offset := (page - 1) * limit + + var err error + results, total, err = store.ListPosts(PostFilter{ + Status: "published", + Search: query, + Limit: limit, + Offset: offset, + }) + if err != nil { + return searchPageProps{}, err + } + } + + return searchPageProps{ + Query: query, + Results: results, + Total: total, + Page: page, + User: auth.GetUser(r), + }, nil +} + +func (s searchPage) Page(props searchPageProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var30 := templ.GetChildren(ctx) + if templ_7745c5c3_Var30 == nil { + templ_7745c5c3_Var30 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var31 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "

Search

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.Query != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 371, Col: 76} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(props.Results) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, post := range props.Results { + templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if baseURL, err := urlFor(ctx, searchPage{}); err == nil { + templ_7745c5c3_Err = pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query))).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

No posts found matching your search.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = layout("Search", props.User).Render(templ.WithChildren(ctx, templ_7745c5c3_Var31), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// HTMX partial rendering for search +func (s searchPage) Results(props searchPageProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var35 := templ.GetChildren(ctx) + if templ_7745c5c3_Var35 == nil { + templ_7745c5c3_Var35 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var37 string + templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Found %d results for \"%s\"", props.Total, props.Query)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/blog-admin/pages.templ`, Line: 397, Col: 73} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(props.Results) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, post := range props.Results { + templ_7745c5c3_Err = postCard(post).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if baseURL, err := urlFor(ctx, searchPage{}); err == nil { + templ_7745c5c3_Err = pagination(props.Page, (props.Total+9)/10, templ.SafeURL(fmt.Sprintf("%s?q=%s", baseURL, props.Query))).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "

No posts found matching your search.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Login page +type loginPage struct{} + +type loginForm struct { + Username string `form:"username"` + Password string `form:"password"` + Redirect string `form:"redirect"` +} + +func (l loginPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService, decoder *form.Decoder) error { + // Already logged in? + if auth.IsAuthenticated(r) { + http.Redirect(w, r, "/admin", http.StatusSeeOther) + return nil + } + + if r.Method == "POST" { + var f loginForm + if err := r.ParseForm(); err != nil { + return err + } + if err := decoder.Decode(&f, r.PostForm); err != nil { + return err + } + + _, err := auth.Login(r, f.Username, f.Password) + if err != nil { + // Show error + return render(w, r, l.Page(auth.GetUser(r), &f, err.Error())) + } + + // Redirect to admin or requested page + redirect := f.Redirect + if redirect == "" { + redirect = "/admin" + } + http.Redirect(w, r, redirect, http.StatusSeeOther) + return nil + } + + // GET request - show form + redirect := r.URL.Query().Get("redirect") + return render(w, r, l.Page(auth.GetUser(r), &loginForm{Redirect: redirect}, "")) +} + +func (l loginPage) Page(user *User, form *loginForm, errorMsg string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var38 := templ.GetChildren(ctx) + if templ_7745c5c3_Var38 == nil { + templ_7745c5c3_Var38 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var39 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "

Login

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if errorMsg != "" { + templ_7745c5c3_Err = alert("error", errorMsg).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if form.Redirect != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = formField("Username", "username", "text", form.Username, nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = formField("Password", "password", "password", "", nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "

Demo credentials: admin / admin123

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = layout("Login", user).Render(templ.WithChildren(ctx, templ_7745c5c3_Var39), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Logout page +type logoutPage struct{} + +func (l logoutPage) ServeHTTP(w http.ResponseWriter, r *http.Request, auth *AuthService) error { + auth.Logout(r) + http.Redirect(w, r, "/", http.StatusSeeOther) + return nil +} + +var _ = templruntime.GeneratedTemplate diff --git a/examples/blog-admin/routes.go b/examples/blog-admin/routes.go new file mode 100644 index 0000000..12d35e9 --- /dev/null +++ b/examples/blog-admin/routes.go @@ -0,0 +1,125 @@ +package main + +import ( + "net/http" + + "github.com/jackielii/structpages" +) + +// Root pages structure demonstrating nested routing +type pages struct { + // Public pages + homePage `route:"/{$} Home"` + postPage `route:"/posts/{slug} "` + categoryPage `route:"/category/{slug} "` + searchPage `route:"/search Search"` + + // Auth pages + loginPage `route:"/login Login"` + logoutPage `route:"POST /logout"` + + // Admin section with nested routes + admin adminPages `route:"/admin Admin"` + + // API endpoints + api apiPages `route:"/api"` +} + +// Admin pages demonstrating nested structure +type adminPages struct { + dashboard `route:"/{$} Dashboard"` + posts adminPostPages `route:"/posts Posts"` + users adminUserPages `route:"/users Users"` + settings adminSettingsPage `route:"/settings Settings"` + mediaLibrary mediaLibraryPage `route:"/media Media Library"` +} + +// Admin post management pages +type adminPostPages struct { + list adminPostListPage `route:"/{$} All Posts"` + new adminPostNewPage `route:"/new New Post"` + edit adminPostEditPage `route:"/{id}/edit"` + delete adminPostDeletePage `route:"POST /{id}/delete"` +} + +// Admin user management pages +type adminUserPages struct { + list adminUserListPage `route:"/{$} All Users"` + new adminUserNewPage `route:"/new New User"` + edit adminUserEditPage `route:"/{id}/edit"` + delete adminUserDeletePage `route:"POST /{id}/delete"` +} + +// API pages for programmatic access +type apiPages struct { + posts apiPostPages `route:"/posts"` + media apiMediaPages `route:"/media"` +} + +type apiPostPages struct { + publish apiPostPublishPage `route:"POST /{id}/publish"` + unpublish apiPostUnpublishPage `route:"POST /{id}/unpublish"` + autosave apiPostAutosavePage `route:"POST /{id}/autosave"` +} + +type apiMediaPages struct { + upload apiMediaUploadPage `route:"POST /upload"` +} + +// Middleware implementations + +// adminPages implements middleware for authentication +func (a adminPages) Middlewares(auth *AuthService) []structpages.MiddlewareFunc { + return []structpages.MiddlewareFunc{ + requireAuthMiddleware(auth), + requireAdminMiddleware(auth), + } +} + +// adminPostPages can have additional middleware +func (p adminPostPages) Middlewares() []structpages.MiddlewareFunc { + return []structpages.MiddlewareFunc{ + csrfProtectionMiddleware, + } +} + +// Helper middleware functions +func requireAuthMiddleware(auth *AuthService) structpages.MiddlewareFunc { + return func(next http.Handler, pn *structpages.PageNode) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := auth.GetUser(r) + if user == nil { + http.Redirect(w, r, "/login?redirect="+r.URL.Path, http.StatusSeeOther) + return + } + next.ServeHTTP(w, r) + }) + } +} + +func requireAdminMiddleware(auth *AuthService) structpages.MiddlewareFunc { + return func(next http.Handler, pn *structpages.PageNode) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := auth.GetUser(r) + if user == nil || user.Role != "admin" { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) + } +} + +func csrfProtectionMiddleware(next http.Handler, pn *structpages.PageNode) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simplified CSRF check - in production use a proper CSRF library + if r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE" { + token := r.FormValue("csrf_token") + if token == "" { + token = r.Header.Get("X-CSRF-Token") + } + // Validate token here + } + next.ServeHTTP(w, r) + }) +} diff --git a/examples/blog-admin/static/admin.css b/examples/blog-admin/static/admin.css new file mode 100644 index 0000000..e62fd42 --- /dev/null +++ b/examples/blog-admin/static/admin.css @@ -0,0 +1,431 @@ +/* Admin styles */ +body.admin { + background: #f5f7fa; +} + +/* Admin navbar */ +.admin-navbar { + background: #2c3e50; + color: white; + padding: 1rem 0; +} + +.admin-navbar .container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.admin-navbar .logo { + color: white; + font-size: 1.25rem; + font-weight: bold; +} + +.admin-navbar .nav-right { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.admin-navbar a { + color: white; +} + +.admin-navbar button { + background: transparent; + border: 1px solid white; + color: white; + padding: 0.25rem 0.75rem; +} + +/* Admin layout */ +.admin-container { + display: grid; + grid-template-columns: 250px 1fr; + min-height: calc(100vh - 60px); +} + +/* Admin sidebar */ +.admin-sidebar { + background: white; + border-right: 1px solid #e1e4e8; +} + +.admin-sidebar ul { + list-style: none; + padding: 0; + margin: 0; +} + +.admin-sidebar a { + display: block; + padding: 0.75rem 1.5rem; + color: #333; + border-bottom: 1px solid #f0f0f0; + transition: background 0.2s; +} + +.admin-sidebar a:hover { + background: #f8f9fa; + text-decoration: none; +} + +.admin-sidebar a.active { + background: #e9ecef; + font-weight: 500; +} + +/* Admin main */ +.admin-main { + padding: 2rem; +} + +.admin-main h1 { + margin: 0 0 2rem; +} + +/* Page header */ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.page-header h1 { + margin: 0; +} + +/* Stats grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +/* Stats card */ +.stats-card { + background: white; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.stats-card h4 { + margin: 0 0 0.5rem; + color: #666; + font-weight: normal; +} + +.stats-card .value { + font-size: 2rem; + font-weight: bold; + color: #333; +} + +.stats-card .change { + color: #28a745; + font-size: 0.875rem; + margin-top: 0.5rem; +} + +/* Dashboard grid */ +.dashboard-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +.dashboard-grid section { + background: white; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.dashboard-grid h2 { + margin: 0 0 1rem; + font-size: 1.25rem; +} + +/* Data table */ +.data-table { + width: 100%; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.data-table th { + background: #f8f9fa; + padding: 0.75rem; + text-align: left; + font-weight: 500; + border-bottom: 1px solid #dee2e6; +} + +.data-table td { + padding: 0.75rem; + border-bottom: 1px solid #f0f0f0; +} + +.data-table tr:last-child td { + border-bottom: none; +} + +.data-table .actions { + white-space: nowrap; +} + +.data-table .actions a, +.data-table .actions button { + margin-right: 0.5rem; + font-size: 0.875rem; + padding: 0.25rem 0.5rem; +} + +/* Status badges */ +.status { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; +} + +.status-draft { + background: #ffeeba; + color: #856404; +} + +.status-published { + background: #d4edda; + color: #155724; +} + +/* Role badges */ +.role { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; +} + +.role-admin { + background: #d1ecf1; + color: #0c5460; +} + +.role-author { + background: #e2e3e5; + color: #383d41; +} + +.role-reader { + background: #f8f9fa; + color: #6c757d; +} + +/* Filters */ +.filters { + background: white; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.filter-form { + display: flex; + gap: 1rem; + align-items: flex-end; +} + +.filter-form input, +.filter-form select { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; +} + +/* Post form */ +.post-form { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 2rem; +} + +.form-grid .sidebar { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-actions { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.checkbox { + display: block; + margin-bottom: 0.5rem; +} + +.checkbox input { + margin-right: 0.5rem; + width: auto; +} + +/* User form */ +.user-form, +.settings-form { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-width: 600px; +} + +/* Media grid */ +.media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +.media-item { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + text-align: center; +} + +.media-item img { + width: 100%; + height: 150px; + object-fit: cover; +} + +.media-item span { + display: block; + padding: 0.5rem; + font-size: 0.875rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +/* Top posts list */ +.top-posts { + list-style: none; + padding: 0; + margin: 0; +} + +.top-posts li { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid #f0f0f0; +} + +.top-posts li:last-child { + border-bottom: none; +} + +/* Card component */ +.card { + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + overflow: hidden; +} + +.card-header { + background: #f8f9fa; + padding: 1rem; + border-bottom: 1px solid #dee2e6; +} + +.card-header h3 { + margin: 0; + font-size: 1.125rem; +} + +.card-body { + padding: 1rem; +} + +/* Modal */ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.5); +} + +.modal-content { + background: white; + padding: 2rem; + border-radius: 8px; + position: relative; + max-width: 500px; + width: 90%; +} + +.modal-content h3 { + margin: 0 0 1rem; +} + +.modal-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 1.5rem; +} + +/* Responsive */ +@media (max-width: 1024px) { + .admin-container { + grid-template-columns: 1fr; + } + + .admin-sidebar { + display: none; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .dashboard-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/examples/blog-admin/static/styles.css b/examples/blog-admin/static/styles.css new file mode 100644 index 0000000..45905ea --- /dev/null +++ b/examples/blog-admin/static/styles.css @@ -0,0 +1,416 @@ +/* Base styles */ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.6; + color: #333; + background: #f5f5f5; +} + +a { + color: #0066cc; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Container */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* Navbar */ +.navbar { + background: white; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + padding: 1rem 0; + margin-bottom: 2rem; +} + +.navbar .container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.navbar .logo { + font-size: 1.5rem; + font-weight: bold; + color: #333; +} + +.nav-links { + display: flex; + gap: 1.5rem; + align-items: center; +} + +.nav-links form { + margin: 0; +} + +/* Hero section */ +.hero { + text-align: center; + padding: 3rem 0; + background: white; + border-radius: 8px; + margin-bottom: 2rem; +} + +.hero h1 { + margin: 0 0 0.5rem; + font-size: 2.5rem; +} + +.hero p { + margin: 0; + color: #666; + font-size: 1.25rem; +} + +/* Grid layouts */ +.home-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 2rem; +} + +.posts-list { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Post card */ +.post-card { + background: white; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.post-card h3 { + margin: 0 0 0.5rem; +} + +.post-card h3 a { + color: #333; +} + +.post-meta { + color: #666; + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.post-meta span:not(:last-child)::after { + content: " • "; +} + +.read-more { + color: #0066cc; + font-weight: 500; +} + +/* Sidebar */ +.sidebar section { + background: white; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.sidebar h3 { + margin: 0 0 1rem; + font-size: 1.25rem; +} + +.sidebar ul { + list-style: none; + padding: 0; + margin: 0; +} + +.sidebar li { + padding: 0.5rem 0; + border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; +} + +.sidebar li:last-child { + border-bottom: none; +} + +.views, .count { + color: #666; + font-size: 0.875rem; +} + +/* Single post */ +.post-single { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.post-single header { + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid #eee; +} + +.post-single h1 { + margin: 0 0 1rem; +} + +.post-content { + font-size: 1.125rem; + line-height: 1.8; + margin-bottom: 2rem; +} + +.post-footer { + padding-top: 1rem; + border-top: 1px solid #eee; + color: #666; +} + +.post-footer > div { + margin-bottom: 0.5rem; +} + +.tag { + background: #f0f0f0; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; +} + +/* Forms */ +.form-field { + margin-bottom: 1.5rem; +} + +.form-field label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.form-field input, +.form-field textarea, +.form-field select { + width: 100%; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.form-field input.error, +.form-field textarea.error, +.form-field select.error { + border-color: #dc3545; +} + +.error-text { + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; + display: block; +} + +/* Buttons */ +.btn, button { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; + text-decoration: none; + display: inline-block; + background: #f0f0f0; + color: #333; +} + +.btn.primary, button[type="submit"] { + background: #0066cc; + color: white; +} + +.btn.primary:hover, button[type="submit"]:hover { + background: #0052a3; +} + +.btn.danger, button.danger { + background: #dc3545; + color: white; +} + +.btn.danger:hover, button.danger:hover { + background: #c82333; +} + +/* Alerts */ +.alert { + padding: 1rem; + border-radius: 4px; + margin-bottom: 1rem; +} + +.alert-success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.alert-error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.alert-warning { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeeba; +} + +/* Search form */ +.search-form { + display: flex; + gap: 1rem; + margin-bottom: 2rem; +} + +.search-form input { + flex: 1; + padding: 0.75rem; + font-size: 1.125rem; +} + +/* Login form */ +.login-form { + max-width: 400px; + margin: 0 auto; + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.login-form h1 { + margin: 0 0 1.5rem; + text-align: center; +} + +.hint { + text-align: center; + color: #666; + margin-top: 1rem; +} + +/* Pagination */ +.pagination { + display: flex; + gap: 0.5rem; + justify-content: center; + margin-top: 2rem; +} + +.pagination a, +.pagination span { + padding: 0.5rem 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; +} + +.pagination .current { + background: #0066cc; + color: white; + border-color: #0066cc; +} + +/* Footer */ +.footer { + background: #333; + color: white; + padding: 2rem 0; + margin-top: 4rem; + text-align: center; +} + +.footer a { + color: #66b3ff; +} + +/* Utilities */ +.inline { + display: inline; +} + +.text-muted { + color: #666; +} + +/* Loading */ +.loading { + text-align: center; + padding: 2rem; +} + +.spinner { + display: inline-block; + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #0066cc; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.spinner-small { + width: 20px; + height: 20px; + border-width: 2px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.htmx-indicator { + position: fixed; + top: 20px; + right: 20px; + display: none; +} + +.htmx-request .htmx-indicator { + display: block; +} + +/* Responsive */ +@media (max-width: 768px) { + .home-grid { + grid-template-columns: 1fr; + } + + .nav-links { + flex-wrap: wrap; + } +} \ No newline at end of file diff --git a/examples/blog-admin/store.go b/examples/blog-admin/store.go new file mode 100644 index 0000000..296795f --- /dev/null +++ b/examples/blog-admin/store.go @@ -0,0 +1,515 @@ +package main + +import ( + "database/sql" + "strings" + "time" +) + +// Store handles all database operations +type Store struct { + db *sql.DB +} + +func NewStore(db *sql.DB) *Store { + return &Store{db: db} +} + +// User operations + +func (s *Store) GetUserByID(id int) (*User, error) { + var u User + err := s.db.QueryRow(` + SELECT id, username, email, password_hash, role, created_at, updated_at + FROM users WHERE id = ? + `, id).Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt) + if err != nil { + return nil, err + } + return &u, nil +} + +func (s *Store) GetUserByUsername(username string) (*User, error) { + var u User + err := s.db.QueryRow(` + SELECT id, username, email, password_hash, role, created_at, updated_at + FROM users WHERE username = ? + `, username).Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt) + if err != nil { + return nil, err + } + return &u, nil +} + +func (s *Store) CreateUser(user *User) error { + result, err := s.db.Exec(` + INSERT INTO users (username, email, password_hash, role) + VALUES (?, ?, ?, ?) + `, user.Username, user.Email, user.PasswordHash, user.Role) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + user.ID = int(id) + return nil +} + +func (s *Store) ListUsers(limit, offset int) ([]*User, int, error) { + // Get total count + var total int + err := s.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&total) + if err != nil { + return nil, 0, err + } + + // Get users + rows, err := s.db.Query(` + SELECT id, username, email, password_hash, role, created_at, updated_at + FROM users + ORDER BY created_at DESC + LIMIT ? OFFSET ? + `, limit, offset) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + var users []*User + for rows.Next() { + var u User + err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt) + if err != nil { + return nil, 0, err + } + users = append(users, &u) + } + return users, total, nil +} + +// Post operations + +func (s *Store) GetPostBySlug(slug string) (*Post, error) { + var p Post + err := s.db.QueryRow(` + SELECT id, slug, title, content, excerpt, author_id, status, + published_at, created_at, updated_at, view_count + FROM posts WHERE slug = ? + `, slug).Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.Excerpt, + &p.AuthorID, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ViewCount) + if err != nil { + return nil, err + } + + // Load author + p.Author, _ = s.GetUserByID(p.AuthorID) + + // Load tags and categories + p.Tags, _ = s.GetPostTags(p.ID) + p.Categories, _ = s.GetPostCategories(p.ID) + + return &p, nil +} + +func (s *Store) GetPostByID(id int) (*Post, error) { + var p Post + err := s.db.QueryRow(` + SELECT id, slug, title, content, excerpt, author_id, status, + published_at, created_at, updated_at, view_count + FROM posts WHERE id = ? + `, id).Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.Excerpt, + &p.AuthorID, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ViewCount) + if err != nil { + return nil, err + } + + // Load author + p.Author, _ = s.GetUserByID(p.AuthorID) + + // Load tags and categories + p.Tags, _ = s.GetPostTags(p.ID) + p.Categories, _ = s.GetPostCategories(p.ID) + + return &p, nil +} + +func (s *Store) ListPosts(filter PostFilter) ([]*Post, int, error) { + query := `SELECT id, slug, title, content, excerpt, author_id, status, + published_at, created_at, updated_at, view_count FROM posts WHERE 1=1` + args := []interface{}{} + + if filter.Status != "" { + query += " AND status = ?" + args = append(args, filter.Status) + } + if filter.AuthorID > 0 { + query += " AND author_id = ?" + args = append(args, filter.AuthorID) + } + if filter.CategoryID > 0 { + query += " AND id IN (SELECT post_id FROM post_categories WHERE category_id = ?)" + args = append(args, filter.CategoryID) + } + if filter.Search != "" { + query += " AND (title LIKE ? OR content LIKE ?)" + searchTerm := "%" + filter.Search + "%" + args = append(args, searchTerm, searchTerm) + } + + // Get total count + var total int + // Build count query from scratch + countQuery := "SELECT COUNT(*) FROM posts WHERE 1=1" + countArgs := []interface{}{} + + if filter.Status != "" { + countQuery += " AND status = ?" + countArgs = append(countArgs, filter.Status) + } + if filter.AuthorID > 0 { + countQuery += " AND author_id = ?" + countArgs = append(countArgs, filter.AuthorID) + } + if filter.CategoryID > 0 { + countQuery += " AND id IN (SELECT post_id FROM post_categories WHERE category_id = ?)" + countArgs = append(countArgs, filter.CategoryID) + } + if filter.Search != "" { + countQuery += " AND (title LIKE ? OR content LIKE ?)" + searchTerm := "%" + filter.Search + "%" + countArgs = append(countArgs, searchTerm, searchTerm) + } + + err := s.db.QueryRow(countQuery, countArgs...).Scan(&total) + if err != nil { + return nil, 0, err + } + + // Add ordering and pagination + query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + args = append(args, filter.Limit, filter.Offset) + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + var posts []*Post + for rows.Next() { + var p Post + err := rows.Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.Excerpt, + &p.AuthorID, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ViewCount) + if err != nil { + return nil, 0, err + } + + // Load author + p.Author, _ = s.GetUserByID(p.AuthorID) + + posts = append(posts, &p) + } + + return posts, total, nil +} + +func (s *Store) CreatePost(post *Post) error { + tx, err := s.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + result, err := tx.Exec(` + INSERT INTO posts (slug, title, content, excerpt, author_id, status, published_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, post.Slug, post.Title, post.Content, post.Excerpt, post.AuthorID, post.Status, post.PublishedAt) + if err != nil { + return err + } + + id, err := result.LastInsertId() + if err != nil { + return err + } + post.ID = int(id) + + // Update tags and categories + if err := s.updatePostTags(tx, post.ID, post.Tags); err != nil { + return err + } + if err := s.updatePostCategories(tx, post.ID, post.Categories); err != nil { + return err + } + + return tx.Commit() +} + +func (s *Store) UpdatePost(post *Post) error { + tx, err := s.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.Exec(` + UPDATE posts + SET slug = ?, title = ?, content = ?, excerpt = ?, + status = ?, published_at = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `, post.Slug, post.Title, post.Content, post.Excerpt, + post.Status, post.PublishedAt, post.ID) + if err != nil { + return err + } + + // Update tags and categories + if err := s.updatePostTags(tx, post.ID, post.Tags); err != nil { + return err + } + if err := s.updatePostCategories(tx, post.ID, post.Categories); err != nil { + return err + } + + return tx.Commit() +} + +func (s *Store) DeletePost(id int) error { + _, err := s.db.Exec("DELETE FROM posts WHERE id = ?", id) + return err +} + +// Category operations + +func (s *Store) ListCategories() ([]*Category, error) { + rows, err := s.db.Query(` + SELECT c.id, c.slug, c.name, c.description, c.created_at, COUNT(pc.post_id) as post_count + FROM categories c + LEFT JOIN post_categories pc ON c.id = pc.category_id + GROUP BY c.id + ORDER BY c.name + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var categories []*Category + for rows.Next() { + var c Category + err := rows.Scan(&c.ID, &c.Slug, &c.Name, &c.Description, &c.CreatedAt, &c.PostCount) + if err != nil { + return nil, err + } + categories = append(categories, &c) + } + return categories, nil +} + +func (s *Store) GetCategoryBySlug(slug string) (*Category, error) { + var c Category + err := s.db.QueryRow(` + SELECT c.id, c.slug, c.name, c.description, c.created_at, COUNT(pc.post_id) as post_count + FROM categories c + LEFT JOIN post_categories pc ON c.id = pc.category_id + WHERE c.slug = ? + GROUP BY c.id + `, slug).Scan(&c.ID, &c.Slug, &c.Name, &c.Description, &c.CreatedAt, &c.PostCount) + if err != nil { + return nil, err + } + return &c, nil +} + +// Tag operations + +func (s *Store) GetPostTags(postID int) ([]Tag, error) { + rows, err := s.db.Query(` + SELECT t.id, t.name + FROM tags t + JOIN post_tags pt ON t.id = pt.tag_id + WHERE pt.post_id = ? + ORDER BY t.name + `, postID) + if err != nil { + return nil, err + } + defer rows.Close() + + var tags []Tag + for rows.Next() { + var t Tag + err := rows.Scan(&t.ID, &t.Name) + if err != nil { + return nil, err + } + tags = append(tags, t) + } + return tags, nil +} + +func (s *Store) GetPostCategories(postID int) ([]Category, error) { + rows, err := s.db.Query(` + SELECT c.id, c.slug, c.name, c.description, c.created_at + FROM categories c + JOIN post_categories pc ON c.id = pc.category_id + WHERE pc.post_id = ? + ORDER BY c.name + `, postID) + if err != nil { + return nil, err + } + defer rows.Close() + + var categories []Category + for rows.Next() { + var c Category + err := rows.Scan(&c.ID, &c.Slug, &c.Name, &c.Description, &c.CreatedAt) + if err != nil { + return nil, err + } + categories = append(categories, c) + } + return categories, nil +} + +// Analytics operations + +func (s *Store) IncrementPostViews(postID int) error { + _, err := s.db.Exec("UPDATE posts SET view_count = view_count + 1 WHERE id = ?", postID) + return err +} + +func (s *Store) RecordPageView(postID *int, path, ipHash, userAgent, referrer string) error { + _, err := s.db.Exec(` + INSERT INTO page_views (post_id, path, ip_hash, user_agent, referrer) + VALUES (?, ?, ?, ?, ?) + `, postID, path, ipHash, userAgent, referrer) + return err +} + +func (s *Store) GetAnalytics(date time.Time) (*Analytics, error) { + // This is a simplified version - in production you'd have more sophisticated analytics + var analytics Analytics + analytics.Date = date + + // Get daily stats + err := s.db.QueryRow(` + SELECT COUNT(DISTINCT ip_hash) as visitors, COUNT(*) as page_views + FROM page_views + WHERE DATE(created_at) = DATE(?) + `, date).Scan(&analytics.Visitors, &analytics.PageViews) + if err != nil { + return nil, err + } + + // Get top posts + rows, err := s.db.Query(` + SELECT p.id, p.title, COUNT(pv.id) as views + FROM posts p + JOIN page_views pv ON p.id = pv.post_id + WHERE DATE(pv.created_at) = DATE(?) + GROUP BY p.id + ORDER BY views DESC + LIMIT 10 + `, date) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var pa PostAnalytics + err := rows.Scan(&pa.PostID, &pa.PostTitle, &pa.Views) + if err != nil { + return nil, err + } + analytics.TopPosts = append(analytics.TopPosts, pa) + } + + return &analytics, nil +} + +// Helper methods + +func (s *Store) updatePostTags(tx *sql.Tx, postID int, tags []Tag) error { + // Delete existing tags + _, err := tx.Exec("DELETE FROM post_tags WHERE post_id = ?", postID) + if err != nil { + return err + } + + // Insert new tags + for _, tag := range tags { + // Get or create tag + var tagID int + err := tx.QueryRow("SELECT id FROM tags WHERE name = ?", tag.Name).Scan(&tagID) + if err == sql.ErrNoRows { + result, err := tx.Exec("INSERT INTO tags (name) VALUES (?)", tag.Name) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + tagID = int(id) + } else if err != nil { + return err + } + + // Link tag to post + _, err = tx.Exec("INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)", postID, tagID) + if err != nil { + return err + } + } + + return nil +} + +func (s *Store) updatePostCategories(tx *sql.Tx, postID int, categories []Category) error { + // Delete existing categories + _, err := tx.Exec("DELETE FROM post_categories WHERE post_id = ?", postID) + if err != nil { + return err + } + + // Insert new categories + for _, cat := range categories { + _, err = tx.Exec("INSERT INTO post_categories (post_id, category_id) VALUES (?, ?)", postID, cat.ID) + if err != nil { + return err + } + } + + return nil +} + +// Filter types + +type PostFilter struct { + Status string + AuthorID int + CategoryID int + Search string + Limit int + Offset int +} + +// Generate slug from title +func GenerateSlug(title string) string { + // Simple slug generation - in production use a proper slugify library + slug := strings.ToLower(title) + slug = strings.ReplaceAll(slug, " ", "-") + slug = strings.ReplaceAll(slug, "'", "") + slug = strings.ReplaceAll(slug, "\"", "") + slug = strings.ReplaceAll(slug, ".", "") + slug = strings.ReplaceAll(slug, ",", "") + slug = strings.ReplaceAll(slug, "!", "") + slug = strings.ReplaceAll(slug, "?", "") + slug = strings.ReplaceAll(slug, "&", "and") + return slug +} diff --git a/examples/blog-admin/utils.go b/examples/blog-admin/utils.go new file mode 100644 index 0000000..3c5bf29 --- /dev/null +++ b/examples/blog-admin/utils.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "net/http" + + "github.com/a-h/templ" +) + +// Render helper for templ components +func render(w http.ResponseWriter, r *http.Request, component templ.Component) error { + templ.Handler(component).ServeHTTP(w, r) + return nil +} + +// Ternary helper for strings +func ternary(condition bool, ifTrue, ifFalse string) string { + if condition { + return ifTrue + } + return ifFalse +} + +// Ternary helper for URLs +func ternaryURL(condition bool, ifTrue, ifFalse templ.SafeURL) templ.SafeURL { + if condition { + return ifTrue + } + return ifFalse +} + +// Helper for conditional URLs with error handling +func ternaryURLWithError(condition bool, ctx context.Context, pageTrue, pageFalse any, args ...any) templ.SafeURL { + if condition { + url, _ := urlFor(ctx, pageTrue) + return url + } + url, _ := urlFor(ctx, pageFalse, args...) + return url +} + +// Helper to get URL or empty string +func urlForOrEmpty(ctx context.Context, page any, args ...any) templ.SafeURL { + url, _ := urlFor(ctx, page, args...) + return url +} diff --git a/examples/htmx/pages_templ.go b/examples/htmx/pages_templ.go index 08e1748..3b2e5de 100644 --- a/examples/htmx/pages_templ.go +++ b/examples/htmx/pages_templ.go @@ -356,7 +356,7 @@ func html() templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, index{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 76, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 76, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -369,7 +369,7 @@ func html() templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, product{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 77, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 77, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -382,7 +382,7 @@ func html() templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, team{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 78, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 78, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -395,7 +395,7 @@ func html() templ.Component { var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, contact{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 79, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 79, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -492,7 +492,7 @@ func errorComp(err error) templ.Component { var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 98, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx/pages.templ`, Line: 98, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { diff --git a/examples/simple/pages_templ.go b/examples/simple/pages_templ.go index 3d7af30..4593007 100644 --- a/examples/simple/pages_templ.go +++ b/examples/simple/pages_templ.go @@ -240,7 +240,7 @@ func html() templ.Component { var templ_7745c5c3_Var10 templ.SafeURL templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, index{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 57, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 57, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -253,7 +253,7 @@ func html() templ.Component { var templ_7745c5c3_Var11 templ.SafeURL templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, product{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 58, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 58, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -266,7 +266,7 @@ func html() templ.Component { var templ_7745c5c3_Var12 templ.SafeURL templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, team{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 59, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 59, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -279,7 +279,7 @@ func html() templ.Component { var templ_7745c5c3_Var13 templ.SafeURL templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(urlFor(ctx, contact{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 60, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/simple/pages.templ`, Line: 60, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { diff --git a/examples/todo/pages_templ.go b/examples/todo/pages_templ.go index 9e6e09d..7de8f7b 100644 --- a/examples/todo/pages_templ.go +++ b/examples/todo/pages_templ.go @@ -60,7 +60,7 @@ func (p index) Page() templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, add{})) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 21, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 21, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -167,7 +167,7 @@ func todoList() templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 1, Col: 0} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 1, Col: 0} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -190,7 +190,7 @@ func todoList() templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, toggle{}, "id", todo.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 92, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 92, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -203,7 +203,7 @@ func todoList() templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Text) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 96, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 96, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -216,7 +216,7 @@ func todoList() templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(urlFor(ctx, deleteTodo{}, "id", todo.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 100, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 100, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -353,7 +353,7 @@ func errorComp(err error) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages.templ`, Line: 232, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/todo/pages.templ`, Line: 232, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil {