diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go
index 3dd8975..b8b2885 100644
--- a/internal/app/handlers/page_handler.go
+++ b/internal/app/handlers/page_handler.go
@@ -128,3 +128,10 @@ func SearchPage(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
+
+func NotFoundPage(ctx *gin.Context) {
+ title := "Potion - Not Found"
+ page := pages.NotFoundPage()
+
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
+}
diff --git a/internal/app/server/server.go b/internal/app/server/server.go
index 5ec329f..f46e8bd 100644
--- a/internal/app/server/server.go
+++ b/internal/app/server/server.go
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"os"
+ "strings"
"github.com/a-h/templ/examples/integration-gin/gintemplrenderer"
"github.com/gin-contrib/cors"
@@ -166,6 +167,7 @@ func (s *Server) Setup() *Server {
router_web.GET("/list", handlers.ListPage)
router_web.GET("/recipe/:id", handlers.RecipePage)
router_web.GET("/search", handlers.SearchPage)
+ router_web.GET("/404", handlers.NotFoundPage)
// WEB state endpoints
router_state.POST("/tags", handlers.NewTag)
@@ -180,5 +182,23 @@ func (s *Server) Setup() *Server {
router_api.POST("/recipe", handlers.CreateRecipe)
router_api.POST("/recipe/search", handlers.SearchRecipes)
+ // Catch un-routed URLS
+ s.Router.NoRoute(func(ctx *gin.Context) {
+ path := ctx.Request.URL.Path
+
+ // TODO: Use constants for errors?
+ if strings.HasPrefix(path, domain.VERSION+domain.API) {
+ ctx.JSON(http.StatusNotFound, gin.H{
+ "status": 404,
+ "error": "API_NOT_FOUND",
+ "message": "The request endpoint does not exist.",
+ "path": path,
+ })
+ return
+ }
+
+ ctx.Redirect(http.StatusSeeOther, domain.WEB_NOT_FOUND)
+ })
+
return s
}
diff --git a/internal/domain/server/routes.go b/internal/domain/server/routes.go
index 24bf0d7..194940e 100644
--- a/internal/domain/server/routes.go
+++ b/internal/domain/server/routes.go
@@ -16,6 +16,7 @@ const WEB_PROFIlE = VERSION + WEB + "/profile"
const WEB_LIST = VERSION + WEB + "/list"
const WEB_RECIPE = VERSION + WEB + "/recipe/%d"
const WEB_SEARCH = VERSION + WEB + "/search"
+const WEB_NOT_FOUND = VERSION + WEB + "/404"
// API prefixed routes
const API_AUTH_LOGIN = VERSION + API + "/auth/login"
diff --git a/internal/templates/pages/notFound.templ b/internal/templates/pages/notFound.templ
new file mode 100644
index 0000000..6e96232
--- /dev/null
+++ b/internal/templates/pages/notFound.templ
@@ -0,0 +1,32 @@
+package templates
+
+import "github.com/haydenhargreaves/Potion/internal/templates/components"
+import "github.com/haydenhargreaves/Potion/internal/domain/server"
+
+templ NotFoundPage() {
+ @components.Navbar("")
+
+
+
+
+
404
+
This page could not be found!
+
Back Home
+
+
+
+}
diff --git a/internal/templates/pages/notFound_templ.go b/internal/templates/pages/notFound_templ.go
new file mode 100644
index 0000000..f6afbe0
--- /dev/null
+++ b/internal/templates/pages/notFound_templ.go
@@ -0,0 +1,56 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.865
+package templates
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import "github.com/haydenhargreaves/Potion/internal/templates/components"
+import "github.com/haydenhargreaves/Potion/internal/domain/server"
+
+func NotFoundPage() 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 = components.Navbar("").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css
index c01b4bc..22cb208 100644
--- a/web/static/css/tailwind.css
+++ b/web/static/css/tailwind.css
@@ -9,8 +9,6 @@
monospace;
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-500: oklch(63.7% 0.237 25.331);
- --color-green-100: oklch(96.2% 0.044 156.743);
- --color-green-600: oklch(62.7% 0.194 149.214);
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128);
@@ -19,6 +17,7 @@
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
+ --color-blue-800: oklch(42.4% 0.199 265.638);
--color-purple-100: oklch(94.6% 0.033 307.174);
--color-purple-200: oklch(90.2% 0.063 306.703);
--color-gray-50: oklch(98.5% 0.002 247.839);
@@ -50,6 +49,12 @@
--text-3xl--line-height: calc(2.25 / 1.875);
--text-4xl: 2.25rem;
--text-4xl--line-height: calc(2.5 / 2.25);
+ --text-5xl: 3rem;
+ --text-5xl--line-height: 1;
+ --text-6xl: 3.75rem;
+ --text-6xl--line-height: 1;
+ --text-8xl: 6rem;
+ --text-8xl--line-height: 1;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
@@ -62,6 +67,7 @@
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+ --animate-bounce: bounce 1s infinite;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
@@ -234,6 +240,9 @@
.static {
position: static;
}
+ .top-1 {
+ top: calc(var(--spacing) * 1);
+ }
.top-1\/2 {
top: calc(1/2 * 100%);
}
@@ -243,6 +252,9 @@
.left-0 {
left: calc(var(--spacing) * 0);
}
+ .left-1 {
+ left: calc(var(--spacing) * 1);
+ }
.left-1\/2 {
left: calc(1/2 * 100%);
}
@@ -264,9 +276,6 @@
.mx-auto {
margin-inline: auto;
}
- .my-0 {
- margin-block: calc(var(--spacing) * 0);
- }
.my-1 {
margin-block: calc(var(--spacing) * 1);
}
@@ -279,6 +288,21 @@
.my-8 {
margin-block: calc(var(--spacing) * 8);
}
+ .-mt-1 {
+ margin-top: calc(var(--spacing) * -1);
+ }
+ .-mt-2 {
+ margin-top: calc(var(--spacing) * -2);
+ }
+ .-mt-3 {
+ margin-top: calc(var(--spacing) * -3);
+ }
+ .-mt-4 {
+ margin-top: calc(var(--spacing) * -4);
+ }
+ .-mt-8 {
+ margin-top: calc(var(--spacing) * -8);
+ }
.mt-2 {
margin-top: calc(var(--spacing) * 2);
}
@@ -356,6 +380,14 @@
width: calc(var(--spacing) * 10);
height: calc(var(--spacing) * 10);
}
+ .size-12 {
+ width: calc(var(--spacing) * 12);
+ height: calc(var(--spacing) * 12);
+ }
+ .size-16 {
+ width: calc(var(--spacing) * 16);
+ height: calc(var(--spacing) * 16);
+ }
.size-32 {
width: calc(var(--spacing) * 32);
height: calc(var(--spacing) * 32);
@@ -383,6 +415,18 @@
.h-8 {
height: calc(var(--spacing) * 8);
}
+ .h-20 {
+ height: calc(var(--spacing) * 20);
+ }
+ .h-24 {
+ height: calc(var(--spacing) * 24);
+ }
+ .h-32 {
+ height: calc(var(--spacing) * 32);
+ }
+ .h-48 {
+ height: calc(var(--spacing) * 48);
+ }
.h-96 {
height: calc(var(--spacing) * 96);
}
@@ -401,12 +445,27 @@
.min-h-screen {
min-height: 100vh;
}
+ .w-1 {
+ width: calc(var(--spacing) * 1);
+ }
+ .w-1\/2 {
+ width: calc(1/2 * 100%);
+ }
.w-1\/3 {
width: calc(1/3 * 100%);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
+ .w-2 {
+ width: calc(var(--spacing) * 2);
+ }
+ .w-2\/3 {
+ width: calc(2/3 * 100%);
+ }
+ .w-3 {
+ width: calc(var(--spacing) * 3);
+ }
.w-3\/4 {
width: calc(3/4 * 100%);
}
@@ -419,6 +478,9 @@
.w-5 {
width: calc(var(--spacing) * 5);
}
+ .w-9 {
+ width: calc(var(--spacing) * 9);
+ }
.w-9\/10 {
width: calc(9/10 * 100%);
}
@@ -437,23 +499,72 @@
.max-w-2xl {
max-width: var(--container-2xl);
}
+ .flex-shrink {
+ flex-shrink: 1;
+ }
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
+ .border-collapse {
+ border-collapse: collapse;
+ }
+ .-translate-x-1 {
+ --tw-translate-x: calc(var(--spacing) * -1);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
.-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
+ .-translate-y-1 {
+ --tw-translate-y: calc(var(--spacing) * -1);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
+ .scale-x-150 {
+ --tw-scale-x: 150%;
+ scale: var(--tw-scale-x) var(--tw-scale-y);
+ }
+ .scale-y-50 {
+ --tw-scale-y: 50%;
+ scale: var(--tw-scale-x) var(--tw-scale-y);
+ }
+ .scale-y-150 {
+ --tw-scale-y: 150%;
+ scale: var(--tw-scale-x) var(--tw-scale-y);
+ }
+ .rotate-12 {
+ rotate: 12deg;
+ }
+ .rotate-45 {
+ rotate: 45deg;
+ }
+ .skew-x-3 {
+ --tw-skew-x: skewX(3deg);
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
+ }
+ .skew-y-3 {
+ --tw-skew-y: skewY(3deg);
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
+ }
+ .transform {
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
+ }
+ .animate-bounce {
+ animation: var(--animate-bounce);
+ }
.cursor-pointer {
cursor: pointer;
}
+ .resize {
+ resize: both;
+ }
.resize-none {
resize: none;
}
@@ -493,6 +604,9 @@
.gap-1 {
gap: calc(var(--spacing) * 1);
}
+ .gap-4 {
+ gap: calc(var(--spacing) * 4);
+ }
.gap-8 {
gap: calc(var(--spacing) * 8);
}
@@ -517,6 +631,9 @@
.gap-y-3 {
row-gap: calc(var(--spacing) * 3);
}
+ .gap-y-4 {
+ row-gap: calc(var(--spacing) * 4);
+ }
.overflow-hidden {
overflow: hidden;
}
@@ -595,6 +712,9 @@
.bg-\[\#f8f8f8\] {
background-color: #f8f8f8;
}
+ .bg-black {
+ background-color: var(--color-black);
+ }
.bg-blue-50 {
background-color: var(--color-blue-50);
}
@@ -720,6 +840,12 @@
.text-center {
text-align: center;
}
+ .font-mono {
+ font-family: var(--font-mono);
+ }
+ .font-sans {
+ font-family: var(--font-sans);
+ }
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
@@ -728,6 +854,18 @@
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
}
+ .text-5xl {
+ font-size: var(--text-5xl);
+ line-height: var(--tw-leading, var(--text-5xl--line-height));
+ }
+ .text-6xl {
+ font-size: var(--text-6xl);
+ line-height: var(--tw-leading, var(--text-6xl--line-height));
+ }
+ .text-8xl {
+ font-size: var(--text-8xl);
+ line-height: var(--tw-leading, var(--text-8xl--line-height));
+ }
.text-base {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
@@ -794,6 +932,9 @@
.text-blue-700 {
color: var(--color-blue-700);
}
+ .text-blue-800 {
+ color: var(--color-blue-800);
+ }
.text-gray-300 {
color: var(--color-gray-300);
}
@@ -821,6 +962,12 @@
.uppercase {
text-transform: uppercase;
}
+ .underline {
+ text-decoration-line: underline;
+ }
+ .opacity-50 {
+ opacity: 50%;
+ }
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -865,6 +1012,10 @@
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
+ .blur {
+ --tw-blur: blur(8px);
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
+ }
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
@@ -873,6 +1024,10 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
+ .duration-100 {
+ --tw-duration: 100ms;
+ transition-duration: 100ms;
+ }
.duration-150 {
--tw-duration: 150ms;
transition-duration: 150ms;
@@ -885,6 +1040,14 @@
--tw-duration: 300ms;
transition-duration: 300ms;
}
+ .duration-500 {
+ --tw-duration: 500ms;
+ transition-duration: 500ms;
+ }
+ .duration-1000 {
+ --tw-duration: 1000ms;
+ transition-duration: 1000ms;
+ }
.ease-in-out {
--tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out);
@@ -1097,6 +1260,11 @@
opacity: 50%;
}
}
+ .motion-safe\:animate-bounce {
+ @media (prefers-reduced-motion: no-preference) {
+ animation: var(--animate-bounce);
+ }
+ }
.sm\:w-3\/4 {
@media (width >= 40rem) {
width: calc(3/4 * 100%);
@@ -1132,9 +1300,9 @@
margin-block: calc(var(--spacing) * 0);
}
}
- .md\:my-2 {
+ .md\:-mt-2 {
@media (width >= 48rem) {
- margin-block: calc(var(--spacing) * 2);
+ margin-top: calc(var(--spacing) * -2);
}
}
.md\:flex {
@@ -1147,6 +1315,12 @@
display: none;
}
}
+ .md\:size-12 {
+ @media (width >= 48rem) {
+ width: calc(var(--spacing) * 12);
+ height: calc(var(--spacing) * 12);
+ }
+ }
.md\:size-32 {
@media (width >= 48rem) {
width: calc(var(--spacing) * 32);
@@ -1165,6 +1339,11 @@
height: calc(var(--spacing) * 64);
}
}
+ .md\:h-24 {
+ @media (width >= 48rem) {
+ height: calc(var(--spacing) * 24);
+ }
+ }
.md\:w-1\/2 {
@media (width >= 48rem) {
width: calc(1/2 * 100%);
@@ -1252,11 +1431,6 @@
padding-block: calc(var(--spacing) * 12);
}
}
- .md\:pt-2 {
- @media (width >= 48rem) {
- padding-top: calc(var(--spacing) * 2);
- }
- }
.md\:pt-14 {
@media (width >= 48rem) {
padding-top: calc(var(--spacing) * 14);
@@ -1285,6 +1459,12 @@
line-height: var(--tw-leading, var(--text-4xl--line-height));
}
}
+ .md\:text-8xl {
+ @media (width >= 48rem) {
+ font-size: var(--text-8xl);
+ line-height: var(--tw-leading, var(--text-8xl--line-height));
+ }
+ }
.md\:text-base {
@media (width >= 48rem) {
font-size: var(--text-base);
@@ -1329,6 +1509,41 @@
inherits: false;
initial-value: 0;
}
+@property --tw-scale-x {
+ syntax: "*";
+ inherits: false;
+ initial-value: 1;
+}
+@property --tw-scale-y {
+ syntax: "*";
+ inherits: false;
+ initial-value: 1;
+}
+@property --tw-scale-z {
+ syntax: "*";
+ inherits: false;
+ initial-value: 1;
+}
+@property --tw-rotate-x {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-rotate-y {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-rotate-z {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-skew-x {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-skew-y {
+ syntax: "*";
+ inherits: false;
+}
@property --tw-border-style {
syntax: "*";
inherits: false;
@@ -1519,12 +1734,30 @@
syntax: "*";
inherits: false;
}
+@keyframes bounce {
+ 0%, 100% {
+ transform: translateY(-25%);
+ animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+ }
+ 50% {
+ transform: none;
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+ }
+}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-scale-z: 1;
+ --tw-rotate-x: initial;
+ --tw-rotate-y: initial;
+ --tw-rotate-z: initial;
+ --tw-skew-x: initial;
+ --tw-skew-y: initial;
--tw-border-style: solid;
--tw-gradient-position: initial;
--tw-gradient-from: #0000;