package docs
import (
"bytes"
"fmt"
"net/http"
"strings"
"bookhoard/internal/database"
"bookhoard/templates"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
// HTTPHandler wraps the docs handler for Echo framework
type HTTPHandler struct {
docs *DocsHandler
}
// NewHTTPHandler creates a new docs HTTP handler
func NewHTTPHandler(docsPath string) *HTTPHandler {
return &HTTPHandler{
docs: NewDocsHandler(docsPath),
}
}
// ShowDocumentation renders a documentation page
func (h *HTTPHandler) ShowDocumentation(c echo.Context) error {
// Get the document path from URL parameter
docPath := c.Param("*")
// Load the document
doc, err := h.docs.LoadDocument(docPath)
if err != nil {
// Try to load as API endpoint
if strings.HasPrefix(docPath, "api/") {
return h.ShowAPIEndpoint(c, strings.TrimPrefix(docPath, "api/"))
}
// Return error page
errorHTML := fmt.Sprintf(`
Document Not Found - Bookhoard
`, err.Error())
return c.HTML(http.StatusNotFound, errorHTML)
}
// Get navigation
nav := h.docs.BuildNavigation()
// Check if user is authenticated via middleware
var user templates.User
if userID := c.Get("user"); userID != nil {
dbUser := userID.(database.Users)
var theme string
if dbUser.Theme.Valid {
theme = dbUser.Theme.String
} else {
theme = "tokyo-night"
}
user = templates.User{
ID: uuid.UUID(dbUser.ID.Bytes).String(),
Username: dbUser.Username,
Email: dbUser.Email,
Role: dbUser.Role,
Theme: theme,
}
} else {
// Create empty user for unauthenticated users
user = templates.User{
ID: "",
Username: "",
Email: "",
Role: "",
Theme: "tokyo-night",
}
}
// Render documentation page
var buf bytes.Buffer
currentPath := c.Request().URL.Path
err = templates.DocsLayout(*nav, *doc, user, currentPath).Render(c.Request().Context(), &buf)
if err != nil {
fmt.Printf("DOCS RENDER ERROR: %v\n", err)
return c.HTML(http.StatusInternalServerError, "Failed to render page: "+err.Error())
}
fmt.Printf("DOCS: Successfully rendered %s (%d bytes)\n", docPath, buf.Len())
return c.HTML(http.StatusOK, buf.String())
}
// ShowAPIEndpoint shows a specific API endpoint documentation
func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error {
// Check if user is logged in
isLoggedIn := false
if userID := c.Get("user_id"); userID != nil {
isLoggedIn = true
}
// Get endpoint data for the explorer
endpointInfo, err := h.docs.GetAPIEndpointData(endpointPath)
if err != nil {
// Endpoint not found in explorer data, fall back to old behavior
return h.showLegacyAPIEndpoint(c, endpointPath, isLoggedIn)
}
// Load the markdown documentation for this endpoint
docPath := "api/" + endpointPath + ".md"
doc, err := h.docs.LoadDocument(docPath)
if err != nil {
errorHTML := `
API Endpoint Not Found - Bookhoard
`
return c.HTML(http.StatusNotFound, errorHTML)
}
// Get navigation
nav := h.docs.BuildNavigation()
// Check if user is authenticated via middleware
var user templates.User
if userID := c.Get("user"); userID != nil {
dbUser := userID.(database.Users)
var theme string
if dbUser.Theme.Valid {
theme = dbUser.Theme.String
} else {
theme = "tokyo-night"
}
user = templates.User{
ID: uuid.UUID(dbUser.ID.Bytes).String(),
Username: dbUser.Username,
Email: dbUser.Email,
Role: dbUser.Role,
Theme: theme,
}
} else {
// Create empty user for unauthenticated users
user = templates.User{
ID: "",
Username: "",
Email: "",
Role: "",
Theme: "tokyo-night",
}
}
// Create API explorer data
explorerData := templates.APIExplorerData{
Endpoint: *endpointInfo,
IsLoggedIn: isLoggedIn,
}
// Render API endpoint page with explorer
var buf bytes.Buffer
currentPath := c.Request().URL.Path
err = templates.DocsLayoutWithExplorer(*nav, *doc, user, explorerData, currentPath).Render(c.Request().Context(), &buf)
if err != nil {
return c.HTML(http.StatusInternalServerError, "Failed to render page")
}
return c.HTML(http.StatusOK, buf.String())
}
// showLegacyAPIEndpoint shows API endpoint documentation without explorer data
func (h *HTTPHandler) showLegacyAPIEndpoint(c echo.Context, endpointPath string, isLoggedIn bool) error {
endpoints := h.docs.GetAPIEndpoints()
// Find the endpoint
var foundEndpoint *APIEndpoint
for _, ep := range endpoints {
if strings.HasSuffix(endpointPath, strings.TrimPrefix(ep.Path, "/api/")) {
foundEndpoint = &ep
break
}
}
if foundEndpoint == nil {
errorHTML := `
API Endpoint Not Found - Bookhoard
`
return c.HTML(http.StatusNotFound, errorHTML)
}
// Get navigation
nav := h.docs.BuildNavigation()
// Check if user is authenticated via middleware
var user templates.User
currentPath := c.Request().URL.Path
if userID := c.Get("user"); userID != nil {
dbUser := userID.(database.Users)
var theme string
if dbUser.Theme.Valid {
theme = dbUser.Theme.String
} else {
theme = "tokyo-night"
}
user = templates.User{
ID: uuid.UUID(dbUser.ID.Bytes).String(),
Username: dbUser.Username,
Email: dbUser.Email,
Role: dbUser.Role,
Theme: theme,
}
} else {
// Create empty user for unauthenticated users
user = templates.User{
ID: "",
Username: "",
Email: "",
Role: "",
Theme: "tokyo-night",
}
}
// Render API endpoint page
var buf bytes.Buffer
err := templates.DocsLayout(*nav, templates.Document{
Title: foundEndpoint.Title,
Content: fmt.Sprintf("Documentation for %s %s
", foundEndpoint.Method, foundEndpoint.Path),
TOC: []templates.TOCItem{},
Breadcrumb: []templates.BreadcrumbItem{
{Title: "Docs", URL: "/docs"},
{Title: "API Reference", URL: "/docs/api"},
{Title: foundEndpoint.Category, URL: "/docs/api"},
{Title: foundEndpoint.Title, URL: ""},
},
Category: "API Reference",
SourceFile: "api",
}, user, currentPath).Render(c.Request().Context(), &buf)
if err != nil {
return c.HTML(http.StatusInternalServerError, "Failed to render page")
}
return c.HTML(http.StatusOK, buf.String())
}
// DocsHome redirects to the documentation index
func (h *HTTPHandler) DocsHome(c echo.Context) error {
return c.Redirect(http.StatusFound, "/docs/index.md")
}
// APIHome shows the API documentation home page
func (h *HTTPHandler) APIHome(c echo.Context) error {
endpoints := h.docs.GetAPIEndpoints()
nav := h.docs.BuildNavigation()
// Check if user is authenticated via middleware
var user templates.User
currentPath := c.Request().URL.Path
if userID := c.Get("user"); userID != nil {
dbUser := userID.(database.Users)
var theme string
if dbUser.Theme.Valid {
theme = dbUser.Theme.String
} else {
theme = "tokyo-night"
}
user = templates.User{
ID: uuid.UUID(dbUser.ID.Bytes).String(),
Username: dbUser.Username,
Email: dbUser.Email,
Role: dbUser.Role,
Theme: theme,
}
} else {
// Create empty user for unauthenticated users
user = templates.User{
ID: "",
Username: "",
Email: "",
Role: "",
Theme: "tokyo-night",
}
}
// Build API documentation content
content := "API Endpoints
Complete API reference for Bookhoard v1.0.
"
for _, ep := range endpoints {
content += fmt.Sprintf(`- %s %s - %s
`,
strings.TrimPrefix(ep.Path, "/api"),
ep.Method,
ep.Path,
ep.Title,
)
}
content += "
"
apiDoc := &templates.Document{
Title: "API Reference",
Content: content,
TOC: []templates.TOCItem{},
Breadcrumb: []templates.BreadcrumbItem{
{Title: "Docs", URL: "/docs"},
{Title: "Developer", URL: "/docs/developer"},
{Title: "API Reference", URL: ""},
},
Category: "API Reference",
SourceFile: "developer/api-reference.md",
}
var buf bytes.Buffer
err := templates.DocsLayout(*nav, *apiDoc, user, currentPath).Render(c.Request().Context(), &buf)
if err != nil {
return c.HTML(http.StatusInternalServerError, "Failed to render page")
}
return c.HTML(http.StatusOK, buf.String())
}
// Search searches through documentation
func (h *HTTPHandler) Search(c echo.Context) error {
query := c.QueryParam("q")
if query == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "query parameter 'q' is required",
})
}
// Get all documents
docs, err := h.docs.ListDocuments()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to list documents",
})
}
// Simple search implementation
var results []SearchResult
queryLower := strings.ToLower(query)
for _, docPath := range docs {
doc, err := h.docs.LoadDocument(docPath)
if err != nil {
continue
}
// Check if title matches
if strings.Contains(strings.ToLower(doc.Title), queryLower) {
results = append(results, SearchResult{
Title: doc.Title,
URL: "/docs/" + strings.TrimSuffix(doc.SourceFile, ".md"),
Type: "Documentation",
})
continue
}
// Check if content matches (simple substring search)
contentLower := strings.ToLower(doc.Content)
if strings.Contains(contentLower, queryLower) {
results = append(results, SearchResult{
Title: doc.Title,
URL: "/docs/" + strings.TrimSuffix(doc.SourceFile, ".md"),
Type: "Documentation",
})
}
}
return c.JSON(http.StatusOK, map[string]interface{}{
"results": results,
"count": len(results),
})
}
// SearchResult represents a search result
type SearchResult struct {
Title string
URL string
Type string
}
// TryEndpoint executes an API endpoint (for the interactive explorer)
func (h *HTTPHandler) TryEndpoint(c echo.Context) error {
// This would execute the actual API request
// For now, return a placeholder
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "API explorer not yet implemented",
})
}
// ServeSearchIndex serves the Lunr.js search index
func (h *HTTPHandler) ServeSearchIndex(c echo.Context) error {
index, err := h.docs.GenerateSearchIndex()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to generate search index",
})
}
return c.JSON(http.StatusOK, index)
}