diff --git a/pkg/appview/handlers/errors.go b/pkg/appview/handlers/errors.go new file mode 100644 index 0000000..bc92172 --- /dev/null +++ b/pkg/appview/handlers/errors.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +// NotFoundHandler handles 404 errors +type NotFoundHandler struct { + Templates *template.Template + RegistryURL string +} + +func (h *NotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + RenderNotFound(w, r, h.Templates, h.RegistryURL) +} + +// RenderNotFound renders the 404 page template. +// Use this from other handlers when a resource is not found. +func RenderNotFound(w http.ResponseWriter, r *http.Request, templates *template.Template, registryURL string) { + w.WriteHeader(http.StatusNotFound) + + data := struct { + PageData + }{ + PageData: NewPageData(r, registryURL), + } + + if err := templates.ExecuteTemplate(w, "404", data); err != nil { + http.Error(w, "Page not found", http.StatusNotFound) + } +} diff --git a/pkg/appview/handlers/repository.go b/pkg/appview/handlers/repository.go index 255e122..93b48f8 100644 --- a/pkg/appview/handlers/repository.go +++ b/pkg/appview/handlers/repository.go @@ -37,7 +37,7 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request // Resolve identifier (handle or DID) to canonical DID and current handle did, resolvedHandle, _, err := atproto.ResolveIdentity(r.Context(), identifier) if err != nil { - http.Error(w, "User not found", http.StatusNotFound) + RenderNotFound(w, r, h.Templates, h.RegistryURL) return } @@ -48,7 +48,7 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } if owner == nil { - http.Error(w, "User not found", http.StatusNotFound) + RenderNotFound(w, r, h.Templates, h.RegistryURL) return } @@ -136,7 +136,7 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request } if len(tagsWithPlatforms) == 0 && len(manifests) == 0 { - http.Error(w, "Repository not found", http.StatusNotFound) + RenderNotFound(w, r, h.Templates, h.RegistryURL) return } diff --git a/pkg/appview/handlers/user.go b/pkg/appview/handlers/user.go index 3136e42..f91f5be 100644 --- a/pkg/appview/handlers/user.go +++ b/pkg/appview/handlers/user.go @@ -23,7 +23,7 @@ func (h *UserPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Resolve identifier (handle or DID) to canonical DID and current handle did, resolvedHandle, pdsEndpoint, err := atproto.ResolveIdentity(r.Context(), identifier) if err != nil { - http.Error(w, "User not found", http.StatusNotFound) + RenderNotFound(w, r, h.Templates, h.RegistryURL) return } diff --git a/pkg/appview/routes/routes.go b/pkg/appview/routes/routes.go index 6f7a60c..b7ac28a 100644 --- a/pkg/appview/routes/routes.go +++ b/pkg/appview/routes/routes.go @@ -224,6 +224,14 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) { } router.Get("/auth/logout", logoutHandler.ServeHTTP) router.Post("/auth/logout", logoutHandler.ServeHTTP) + + // Custom 404 handler + router.NotFound(middleware.OptionalAuth(deps.SessionStore, deps.Database)( + &uihandlers.NotFoundHandler{ + Templates: deps.Templates, + RegistryURL: registryURL, + }, + ).ServeHTTP) } // CORSMiddleware returns a middleware that sets CORS headers for API endpoints diff --git a/pkg/appview/static/css/style.css b/pkg/appview/static/css/style.css index f0eed57..e30ed10 100644 --- a/pkg/appview/static/css/style.css +++ b/pkg/appview/static/css/style.css @@ -2373,3 +2373,59 @@ a.license-badge:hover { padding: 0.75rem; } } + +/* 404 Error Page */ +.error-page { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 60px); + text-align: center; + padding: 2rem; +} + +.error-content { + max-width: 480px; +} + +.error-icon { + width: 80px; + height: 80px; + color: var(--secondary); + margin-bottom: 1.5rem; +} + +.error-code { + font-size: 8rem; + font-weight: 700; + color: var(--primary); + line-height: 1; + margin-bottom: 0.5rem; +} + +.error-content h1 { + font-size: 2rem; + margin-bottom: 0.75rem; + color: var(--fg); +} + +.error-content p { + font-size: 1.125rem; + color: var(--secondary); + margin-bottom: 2rem; +} + +@media (max-width: 768px) { + .error-code { + font-size: 5rem; + } + + .error-icon { + width: 60px; + height: 60px; + } + + .error-content h1 { + font-size: 1.5rem; + } +} diff --git a/pkg/appview/templates/pages/404.html b/pkg/appview/templates/pages/404.html new file mode 100644 index 0000000..ab2ac47 --- /dev/null +++ b/pkg/appview/templates/pages/404.html @@ -0,0 +1,22 @@ +{{ define "404" }} + + + + 404 - Lost at Sea | ATCR + {{ template "head" . }} + + + {{ template "nav-simple" . }} +
+
+ +
404
+

Lost at Sea

+

The page you're looking for has drifted into uncharted waters.

+ Return to Port +
+
+ + + +{{ end }}