From d8830fbbc08309fbf0d997da94fd643de6d3ee69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 30 Oct 2024 13:15:01 +0100 Subject: [PATCH] jinja --- Cargo.lock | 22 + Cargo.toml | 3 + assets/styles.css | 971 +----------------- src/actors/search.rs | 285 ++--- src/main.rs | 8 +- src/routes.rs | 53 +- tailwind.config.js | 2 +- templates/{base.html => base.jinja} | 4 +- templates/{index.html => index.jinja} | 2 +- templates/nav.html | 37 - templates/nav.jinja | 80 ++ .../{recipe_card.html => recipe_card.jinja} | 0 templates/recipies/create_form.jinja | 38 + templates/recipies/show.html | 11 +- templates/recipies/show.jinja | 83 ++ templates/recipies/update_form.html | 0 .../{create_form.html => update_form.jinja} | 0 templates/{search.html => search.jinja} | 9 +- templates/sign_in/{form.html => form.jinja} | 4 +- templates/top_bar.html | 8 - templates/top_bar.jinja | 15 + 21 files changed, 508 insertions(+), 1127 deletions(-) rename templates/{base.html => base.jinja} (87%) rename templates/{index.html => index.jinja} (96%) delete mode 100644 templates/nav.html create mode 100644 templates/nav.jinja rename templates/{recipe_card.html => recipe_card.jinja} (100%) create mode 100644 templates/recipies/create_form.jinja create mode 100644 templates/recipies/show.jinja delete mode 100644 templates/recipies/update_form.html rename templates/recipies/{create_form.html => update_form.jinja} (100%) rename templates/{search.html => search.jinja} (72%) rename templates/sign_in/{form.html => form.jinja} (97%) delete mode 100644 templates/top_bar.html create mode 100644 templates/top_bar.jinja diff --git a/Cargo.lock b/Cargo.lock index 565fba5..63700d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1143,6 +1143,7 @@ dependencies = [ "tempfile", "tracing", "tracing-subscriber", + "tracing-test", "uuid", ] @@ -4943,6 +4944,27 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.82", +] + [[package]] name = "typed-arena" version = "2.0.2" diff --git a/Cargo.toml b/Cargo.toml index 253695f..fa2ef44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,6 @@ tantivy = "0.22.0" tempfile = "3.13.0" [build-dependencies] + +[dev-dependencies] +tracing-test = "0.2.5" diff --git a/assets/styles.css b/assets/styles.css index 280e6c0..b4c01e4 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -659,6 +659,41 @@ html{ --btn-focus-scale: 1; } +@media (prefers-color-scheme: dark){ + :root{ + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; + } +} + [data-theme=corporate]{ color-scheme: light; --b2: 93% 0 0; @@ -725,194 +760,6 @@ html{ --bc: 74.6477% 0.0216 264.435964; } -.alert{ - display: grid; - width: 100%; - grid-auto-flow: row; - align-content: flex-start; - align-items: center; - justify-items: center; - gap: 1rem; - text-align: center; - border-radius: var(--rounded-box, 1rem); - border-width: 1px; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - padding: 1rem; - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --alert-bg: var(--fallback-b2,oklch(var(--b2)/1)); - --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); - background-color: var(--alert-bg); -} - -@media (min-width: 640px){ - .alert{ - grid-auto-flow: column; - grid-template-columns: auto minmax(auto,1fr); - justify-items: start; - text-align: start; - } -} - -.avatar.placeholder > div{ - display: flex; - align-items: center; - justify-content: center; -} - -.badge{ - display: inline-flex; - align-items: center; - justify-content: center; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - height: 1.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - width: -moz-fit-content; - width: fit-content; - padding-left: 0.563rem; - padding-right: 0.563rem; - border-radius: var(--rounded-badge, 1.9rem); - border-width: 1px; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); -} - -@media (hover:hover){ - .label a:hover{ - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - } - - .menu li > *:not(ul, .menu-title, details, .btn):active, -.menu li > *:not(ul, .menu-title, details, .btn).active, -.menu li > details > summary:active{ - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); - } -} - -.btn{ - display: inline-flex; - height: 3rem; - min-height: 3rem; - flex-shrink: 0; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - flex-wrap: wrap; - align-items: center; - justify-content: center; - border-radius: var(--rounded-btn, 0.5rem); - border-color: transparent; - border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity)); - padding-left: 1rem; - padding-right: 1rem; - text-align: center; - font-size: 0.875rem; - line-height: 1em; - gap: 0.5rem; - font-weight: 600; - text-decoration-line: none; - transition-duration: 200ms; - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - border-width: var(--border-btn, 1px); - transition-property: color, background-color, border-color, opacity, box-shadow, transform; - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); - background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity)); - --tw-bg-opacity: 1; - --tw-border-opacity: 1; -} - -.btn-disabled, - .btn[disabled], - .btn:disabled{ - pointer-events: none; -} - -:where(.btn:is(input[type="checkbox"])), -:where(.btn:is(input[type="radio"])){ - width: auto; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.btn:is(input[type="checkbox"]):after, -.btn:is(input[type="radio"]):after{ - --tw-content: attr(aria-label); - content: var(--tw-content); -} - -.card{ - position: relative; - display: flex; - flex-direction: column; - border-radius: var(--rounded-box, 1rem); -} - -.card:focus{ - outline: 2px solid transparent; - outline-offset: 2px; -} - -.card figure{ - display: flex; - align-items: center; - justify-content: center; -} - -.card.image-full{ - display: grid; -} - -.card.image-full:before{ - position: relative; - content: ""; - z-index: 10; - border-radius: var(--rounded-box, 1rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - opacity: 0.75; -} - -.card.image-full:before, - .card.image-full > *{ - grid-column-start: 1; - grid-row-start: 1; -} - -.card.image-full > figure img{ - height: 100%; - -o-object-fit: cover; - object-fit: cover; -} - -.card.image-full > .card-body{ - position: relative; - z-index: 20; - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); -} - .divider{ display: flex; flex-direction: row; @@ -934,167 +781,6 @@ html{ background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); } -@media (hover: hover){ - .btn:hover{ - --tw-border-opacity: 1; - border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); - } - - @supports (color: color-mix(in oklab, black, black)){ - .btn:hover{ - background-color: color-mix( - in oklab, - oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, - black - ); - border-color: color-mix( - in oklab, - oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, - black - ); - } - } - - @supports not (color: oklch(0% 0 0)){ - .btn:hover{ - background-color: var(--btn-color, var(--fallback-b2)); - border-color: var(--btn-color, var(--fallback-b2)); - } - } - - .btn.glass:hover{ - --glass-opacity: 25%; - --glass-border-opacity: 15%; - } - - .btn-disabled:hover, - .btn[disabled]:hover, - .btn:disabled:hover{ - --tw-border-opacity: 0; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-bg-opacity: 0.2; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; - } - - @supports (color: color-mix(in oklab, black, black)){ - .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover{ - background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); - } - } - - :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover{ - cursor: pointer; - outline: 2px solid transparent; - outline-offset: 2px; - } - - @supports (color: oklch(0% 0 0)){ - :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover{ - background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); - } - } -} - -.label{ - display: flex; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - align-items: center; - justify-content: space-between; - padding-left: 0.25rem; - padding-right: 0.25rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.input{ - flex-shrink: 1; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - height: 3rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 1rem; - line-height: 2; - line-height: 1.5rem; - border-radius: var(--rounded-btn, 0.5rem); - border-width: 1px; - border-color: transparent; - --tw-bg-opacity: 1; - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); -} - -.input[type="number"]::-webkit-inner-spin-button, -.input-md[type="number"]::-webkit-inner-spin-button{ - margin-top: -1rem; - margin-bottom: -1rem; - margin-inline-end: -1rem; -} - -.link{ - cursor: pointer; - text-decoration-line: underline; -} - -.menu{ - display: flex; - flex-direction: column; - flex-wrap: wrap; - font-size: 0.875rem; - line-height: 1.25rem; - padding: 0.5rem; -} - -.menu :where(li ul){ - position: relative; - white-space: nowrap; - margin-inline-start: 1rem; - padding-inline-start: 0.5rem; -} - -.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)){ - display: grid; - grid-auto-flow: column; - align-content: flex-start; - align-items: center; - gap: 0.5rem; - grid-auto-columns: minmax(auto, max-content) auto max-content; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.menu li.disabled{ - cursor: not-allowed; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - color: var(--fallback-bc,oklch(var(--bc)/0.3)); -} - -.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)){ - display: none; -} - -:where(.menu li){ - position: relative; - display: flex; - flex-shrink: 0; - flex-direction: column; - flex-wrap: wrap; - align-items: stretch; -} - -:where(.menu li) .badge{ - justify-self: end; -} - .steps{ display: inline-grid; grid-auto-flow: column; @@ -1115,80 +801,6 @@ html{ min-width: 4rem; } -.alert-error{ - border-color: var(--fallback-er,oklch(var(--er)/0.2)); - --tw-text-opacity: 1; - color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); - --alert-bg: var(--fallback-er,oklch(var(--er)/1)); - --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); -} - -.btm-nav > * .label{ - font-size: 1rem; - line-height: 1.5rem; -} - -@media (prefers-reduced-motion: no-preference){ - .btn{ - animation: button-pop var(--animation-btn, 0.25s) ease-out; - } -} - -.btn:active:hover, - .btn:active:focus{ - animation: button-pop 0s ease-out; - transform: scale(var(--btn-focus-scale, 0.97)); -} - -@supports not (color: oklch(0% 0 0)){ - .btn{ - background-color: var(--btn-color, var(--fallback-b2)); - border-color: var(--btn-color, var(--fallback-b2)); - } -} - -.btn:focus-visible{ - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; -} - -.btn.glass{ - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - outline-color: currentColor; -} - -.btn.glass.btn-active{ - --glass-opacity: 25%; - --glass-border-opacity: 15%; -} - -.btn.btn-disabled, - .btn[disabled], - .btn:disabled{ - --tw-border-opacity: 0; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-bg-opacity: 0.2; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - --tw-text-opacity: 0.2; -} - -.btn:is(input[type="checkbox"]:checked), -.btn:is(input[type="radio"]:checked){ - --tw-border-opacity: 1; - border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); -} - -.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible{ - outline-color: var(--fallback-p,oklch(var(--p)/1)); -} - @keyframes button-pop{ 0%{ transform: scale(var(--btn-focus-scale, 0.98)); @@ -1203,44 +815,6 @@ html{ } } -.card :where(figure:first-child){ - overflow: hidden; - border-start-start-radius: inherit; - border-start-end-radius: inherit; - border-end-start-radius: unset; - border-end-end-radius: unset; -} - -.card :where(figure:last-child){ - overflow: hidden; - border-start-start-radius: unset; - border-start-end-radius: unset; - border-end-start-radius: inherit; - border-end-end-radius: inherit; -} - -.card:focus-visible{ - outline: 2px solid currentColor; - outline-offset: 2px; -} - -.card.bordered{ - border-width: 1px; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); -} - -.card.compact .card-body{ - padding: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.card.image-full :where(figure){ - overflow: hidden; - border-radius: inherit; -} - @keyframes checkmark{ 0%{ background-position-y: 5px; @@ -1259,210 +833,6 @@ html{ gap: 1rem; } -.input input{ - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - background-color: transparent; -} - -.input input:focus{ - outline: 2px solid transparent; - outline-offset: 2px; -} - -.input[list]::-webkit-calendar-picker-indicator{ - line-height: 1em; -} - -.input-bordered{ - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.input:focus, - .input:focus-within{ - box-shadow: none; - border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); -} - -.input:has(> input[disabled]), - .input-disabled, - .input:disabled, - .input[disabled]{ - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - color: var(--fallback-bc,oklch(var(--bc)/0.4)); -} - -.input:has(> input[disabled])::-moz-placeholder, .input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder{ - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -.input:has(> input[disabled])::placeholder, - .input-disabled::placeholder, - .input:disabled::placeholder, - .input[disabled]::placeholder{ - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); - --tw-placeholder-opacity: 0.2; -} - -.input:has(> input[disabled]) > input[disabled]{ - cursor: not-allowed; -} - -.input::-webkit-date-and-time-value{ - text-align: inherit; -} - -.join > :where(*:not(:first-child)):is(.btn){ - margin-inline-start: calc(var(--border-btn) * -1); -} - -.link:focus{ - outline: 2px solid transparent; - outline-offset: 2px; -} - -.link:focus-visible{ - outline: 2px solid currentColor; - outline-offset: 2px; -} - -:where(.menu li:empty){ - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - opacity: 0.1; - margin: 0.5rem 1rem; - height: 1px; -} - -.menu :where(li ul):before{ - position: absolute; - bottom: 0.75rem; - inset-inline-start: 0px; - top: 0.75rem; - width: 1px; - --tw-bg-opacity: 1; - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); - opacity: 0.1; - content: ""; -} - -.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), -.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)){ - border-radius: var(--rounded-btn, 0.5rem); - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - text-align: start; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - text-wrap: balance; -} - -:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):is(summary):not(.active, .btn):focus-visible, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):is(summary):not(.active, .btn):focus-visible{ - cursor: pointer; - background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); - --tw-text-opacity: 1; - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); - outline: 2px solid transparent; - outline-offset: 2px; -} - -.menu li > *:not(ul, .menu-title, details, .btn):active, -.menu li > *:not(ul, .menu-title, details, .btn).active, -.menu li > details > summary:active{ - --tw-bg-opacity: 1; - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); -} - -.menu :where(li > details > summary)::-webkit-details-marker{ - display: none; -} - -.menu :where(li > details > summary):after, -.menu :where(li > .menu-dropdown-toggle):after{ - justify-self: end; - display: block; - margin-top: -0.5rem; - height: 0.5rem; - width: 0.5rem; - transform: rotate(45deg); - transition-property: transform, margin-top; - transition-duration: 0.3s; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - content: ""; - transform-origin: 75% 75%; - box-shadow: 2px 2px; - pointer-events: none; -} - -.menu :where(li > details[open] > summary):after, -.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after{ - transform: rotate(225deg); - margin-top: 0; -} - -.mockup-browser .mockup-browser-toolbar .input{ - position: relative; - margin-left: auto; - margin-right: auto; - display: block; - height: 1.75rem; - width: 24rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - padding-left: 2rem; - direction: ltr; -} - -.mockup-browser .mockup-browser-toolbar .input:before{ - content: ""; - position: absolute; - left: 0.5rem; - top: 50%; - aspect-ratio: 1 / 1; - height: 0.75rem; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-radius: 9999px; - border-width: 2px; - border-color: currentColor; - opacity: 0.6; -} - -.mockup-browser .mockup-browser-toolbar .input:after{ - content: ""; - position: absolute; - left: 1.25rem; - top: 50%; - height: 0.5rem; - --tw-translate-y: 25%; - --tw-rotate: -45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-radius: 9999px; - border-width: 1px; - border-color: currentColor; - opacity: 0.6; -} - @keyframes modal-pop{ 0%{ opacity: 0; @@ -1664,15 +1034,6 @@ html{ grid-template-rows: repeat(1, minmax(0, 1fr)); } -.join.join-vertical > :where(*:not(:first-child)):is(.btn){ - margin-top: calc(var(--border-btn) * -1); -} - -.join.join-horizontal > :where(*:not(:first-child)):is(.btn){ - margin-inline-start: calc(var(--border-btn) * -1); - margin-top: 0px; -} - .steps-horizontal .step{ grid-template-rows: 40px 1fr; grid-template-columns: auto; @@ -1720,14 +1081,6 @@ html{ margin: 1rem; } -.m-auto{ - margin: auto; -} - -.ml-2{ - margin-left: 0.5rem; -} - .ml-auto{ margin-left: auto; } @@ -1748,74 +1101,18 @@ html{ display: block; } -.inline-block{ - display: inline-block; -} - .flex{ display: flex; } -.hidden{ - display: none; -} - -.h-32{ - height: 8rem; -} - -.h-4{ - height: 1rem; -} - -.h-5{ - height: 1.25rem; -} - -.h-6{ - height: 1.5rem; -} - -.w-4{ - width: 1rem; -} - .w-40{ width: 10rem; } -.w-5{ - width: 1.25rem; -} - -.w-6{ - width: 1.5rem; -} - .w-full{ width: 100%; } -.shrink-0{ - flex-shrink: 0; -} - -.grow{ - flex-grow: 1; -} - -.grow-0{ - flex-grow: 0; -} - -.transform{ - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.cursor-pointer{ - cursor: pointer; -} - .list-inside{ list-style-position: inside; } @@ -1832,30 +1129,14 @@ html{ flex-direction: column; } -.flex-wrap{ - flex-wrap: wrap; -} - .items-center{ align-items: center; } -.justify-end{ - justify-content: flex-end; -} - .justify-center{ justify-content: center; } -.justify-between{ - justify-content: space-between; -} - -.gap-2{ - gap: 0.5rem; -} - .gap-3{ gap: 0.75rem; } @@ -1868,10 +1149,6 @@ html{ gap: 1.75rem; } -.gap-8{ - gap: 2rem; -} - .rounded-full{ border-radius: 9999px; } @@ -1880,37 +1157,11 @@ html{ border-radius: 0.75rem; } -.border-b{ - border-bottom-width: 1px; -} - -.border-gray-100{ - --tw-border-opacity: 1; - border-color: rgb(243 244 246 / var(--tw-border-opacity)); -} - .bg-gray-300{ --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity)); } -.stroke-current{ - stroke: currentColor; -} - -.object-cover{ - -o-object-fit: cover; - object-fit: cover; -} - -.p-12{ - padding: 3rem; -} - -.p-4{ - padding: 1rem; -} - .px-4{ padding-left: 1rem; padding-right: 1rem; @@ -1925,51 +1176,15 @@ html{ text-align: center; } -.text-right{ - text-align: right; -} - -.font-body{ - font-family: Nunito; -} - -.text-6xl{ - font-size: 3.75rem; - line-height: 1; -} - .text-base{ font-size: 1rem; line-height: 1.5rem; } -.text-sm{ - font-size: 0.875rem; - line-height: 1.25rem; -} - .font-bold{ font-weight: 700; } -.font-semibold{ - font-weight: 600; -} - -.uppercase{ - text-transform: uppercase; -} - -.text-gray-500{ - --tw-text-opacity: 1; - color: rgb(107 114 128 / var(--tw-text-opacity)); -} - -.text-gray-600{ - --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity)); -} - .text-gray-700{ --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity)); @@ -1985,30 +1200,6 @@ html{ color: rgb(68 64 60 / var(--tw-text-opacity)); } -.opacity-70{ - opacity: 0.7; -} - -.transition{ - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.duration-300{ - transition-duration: 300ms; -} - -.ease-linear{ - transition-timing-function: linear; -} - -.ease-out{ - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); -} - .section-separator{ border-width: 0px; border-bottom-width: 2px; @@ -2079,43 +1270,16 @@ html{ color: rgb(136 19 55 ); } -.hover\:scale-105:hover{ - --tw-scale-x: 1.05; - --tw-scale-y: 1.05; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - .hover\:bg-gray-400:hover{ --tw-bg-opacity: 1; background-color: rgb(156 163 175 / var(--tw-bg-opacity)); } -.hover\:text-gray-600:hover{ - --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity)); -} - -.hover\:shadow-lg:hover{ - --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.hover\:shadow-md:hover{ - --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - @media (min-width: 640px){ .sm\:rounded-3xl{ border-radius: 1.5rem; } - .sm\:border-2{ - border-width: 2px; - } - .sm\:text-base{ font-size: 1rem; line-height: 1.5rem; @@ -2128,80 +1292,15 @@ html{ } @media (min-width: 768px){ - .md\:col-span-1{ - grid-column: span 1 / span 1; - } - - .md\:col-span-3{ - grid-column: span 3 / span 3; - } - - .md\:flex{ - display: flex; - } - - .md\:hidden{ - display: none; - } - - .md\:h-48{ - height: 12rem; - } - .md\:w-3\/4{ width: 75%; } - .md\:w-\[225px\]{ - width: 225px; - } - - .md\:flex-row{ - flex-direction: row; - } - - .md\:justify-end{ - justify-content: flex-end; - } - .md\:text-left{ text-align: left; } } -@media (min-width: 1024px){ - .lg\:w-800{ - width: 50rem; - } - - .lg\:w-\[400px\]{ - width: 400px; - } - - .lg\:rounded-lg{ - border-radius: 0.5rem; - } - - .lg\:border{ - border-width: 1px; - } - - .lg\:border-0{ - border-width: 0px; - } - - .lg\:border-gray-300{ - --tw-border-opacity: 1; - border-color: rgb(209 213 219 / var(--tw-border-opacity)); - } - - .lg\:shadow{ - --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - } -} - @media (prefers-color-scheme: dark){ .dark\:bg-gray-700{ --tw-bg-opacity: 1; diff --git a/src/actors/search.rs b/src/actors/search.rs index b73a12e..ce73c0c 100644 --- a/src/actors/search.rs +++ b/src/actors/search.rs @@ -1,11 +1,16 @@ +use std::path::Path; use std::sync::{Arc, Mutex}; use actix::prelude::*; use tantivy::collector::TopDocs; use tantivy::query::QueryParser; -use tantivy::{doc, Index, IndexWriter, ReloadPolicy, Searcher}; +use tantivy::{doc, Index, IndexReader, IndexWriter, ReloadPolicy}; use tantivy::{schema::*, TantivyError}; +const ID: &'static str = "id"; +const TITLE: &'static str = "title"; +const SUMMARY: &'static str = "summary"; + #[derive(Debug, Clone, derive_more::Deref)] pub struct Search(pub Addr); @@ -29,46 +34,43 @@ impl From for RecipeRecord { pub struct Inner { writer: IndexWriter, schema: Schema, - searcher: Searcher, index: Index, + reader: IndexReader, } pub struct SearchEngine(Arc>); impl SearchEngine { - pub fn build() -> Result { - let index_path = std::path::Path::new("./indices"); + pub fn build(index_path: &Path) -> Result { std::fs::create_dir_all(&index_path).expect("Failed to create indices directory"); let mut schema_builder = Schema::builder(); - schema_builder.add_u64_field("id", INDEXED); - schema_builder.add_text_field("title", TEXT); - schema_builder.add_text_field("summary", TEXT); + + schema_builder.add_u64_field(ID, STORED); + schema_builder.add_text_field(TITLE, TEXT | STORED); + schema_builder.add_text_field(SUMMARY, TEXT); let schema = schema_builder.build(); let index = Index::create_in_dir(&index_path, schema.clone()) .or_else(|_| Index::open_in_dir(&index_path)) .expect("Failed to construct indices directory"); - let index_writer: IndexWriter = index.writer(50_000_000)?; - + let writer: IndexWriter = index.writer(50_000_000)?; let reader = index .reader_builder() .reload_policy(ReloadPolicy::OnCommitWithDelay) - .try_into() - .unwrap(); - let searcher = reader.searcher(); + .try_into()?; Ok(Self(Arc::new(Mutex::new(Inner { - writer: index_writer, + writer, schema, - searcher, index, + reader, })))) } } impl actix::Actor for SearchEngine { - type Context = actix::Context; + type Context = actix::SyncContext; } #[derive(Debug, Message)] @@ -78,32 +80,30 @@ pub struct CreateRecipe { } impl Handler for SearchEngine { - type Result = actix::ResponseActFuture>; + type Result = Result; fn handle(&mut self, msg: CreateRecipe, _ctx: &mut Self::Context) -> Self::Result { - let inner = self.0.clone(); - Box::pin( - async move { - let mut shared = inner.lock().unwrap(); - - let id = shared.schema.get_field("id").unwrap(); - let title = shared.schema.get_field("summary").unwrap(); - let summary = shared.schema.get_field("summary").unwrap(); - - let msg = msg.record; - let n = shared.writer.add_document(doc! { - id => msg.id, - title => msg.title, - summary => msg.summary.unwrap_or_default(), - })?; - shared.writer.commit()?; - - Ok(n) - } - .into_actor(self), - ) + create(self.0.clone(), msg) } } +fn create(inner: Arc>, msg: CreateRecipe) -> Result { + let mut shared = inner.lock().unwrap(); + + let id = shared.schema.get_field(ID).unwrap(); + let title = shared.schema.get_field(TITLE).unwrap(); + let summary = shared.schema.get_field(SUMMARY).unwrap(); + + let msg = msg.record; + let n = shared.writer.add_document(doc! { + id => msg.id, + title => msg.title, + summary => msg.summary.unwrap_or_default(), + })?; + tracing::debug!("attemp commit"); + shared.writer.commit()?; + + Ok(n) +} #[derive(Debug, Message)] #[rtype(result = "Result,TantivyError>")] @@ -112,64 +112,62 @@ pub struct Find { } impl Handler for SearchEngine { - type Result = actix::ResponseActFuture, TantivyError>>; + type Result = Result, TantivyError>; fn handle(&mut self, msg: Find, _ctx: &mut Self::Context) -> Self::Result { - let inner = self.0.clone(); - Box::pin( - async move { - let shared = inner.lock().unwrap(); - - let id = shared.schema.get_field("id").unwrap(); - let title = shared.schema.get_field("summary").unwrap(); - let summary = shared.schema.get_field("summary").unwrap(); - - let query_parser = QueryParser::for_index(&shared.index, vec![title, summary]); - let query = msg - .query - .split_whitespace() - .map(|piece| { - piece - .chars() - .filter(|c| c.is_alphabetic()) - .collect::() - }) - .filter(|s| !s.trim().is_empty()) - .collect::>() - .join(" OR "); - tracing::debug!("Query is: {query:?}"); - let query = query_parser.parse_query(&query).expect("invalid query"); - - let rows = shared.searcher.search(&query, &TopDocs::with_limit(100))?; - - let ids = rows - .into_iter() - .filter_map(|row| { - tracing::debug!("tantivy row: {row:?}"); - let doc: Option = shared.searcher.doc(row.1).ok(); - doc - }) - .fold(Vec::with_capacity(1_000), |agg, doc| { - let json = doc.to_json(&shared.schema); - tracing::debug!("tantivy doc: {doc:?} {json:?}"); - doc.get_all(id) - .filter_map(|id| { - tracing::debug!("tantivy id: {id:?}"); - id.as_u64() - }) - .fold(agg, |mut agg, id| { - agg.push(id); - agg - }) - }); - - Ok(ids) - } - .into_actor(self), - ) + find(self.0.clone(), msg) } } +fn find(inner: Arc>, msg: Find) -> Result, TantivyError> { + let shared = inner.lock().unwrap(); + + let id = shared.schema.get_field(ID).unwrap(); + let title = shared.schema.get_field(TITLE).unwrap(); + let summary = shared.schema.get_field(SUMMARY).unwrap(); + + let query_parser = QueryParser::for_index(&shared.index, vec![id, title, summary]); + let query = msg + .query + .split_whitespace() + .map(|piece| { + piece + .chars() + .filter(|c| c.is_alphabetic()) + .collect::() + }) + .filter(|s| !s.trim().is_empty()) + .map(|s| format!("(title:{s} OR summary:{s})", s = s.trim())) + .collect::>() + .join(" OR "); + tracing::debug!("Query is: {query:?}"); + let query = query_parser.parse_query(&query).expect("invalid query"); + + shared.reader.reload()?; + let searcher = shared.reader.searcher(); + let rows = searcher.search(&query, &TopDocs::with_limit(10))?; + + let ids = rows + .into_iter() + .filter_map(|(_score, addr)| { + let doc: Option = searcher.doc(addr).ok(); + doc + }) + .fold(Vec::with_capacity(100), |mut agg, doc| { + let Some(id) = doc.into_iter().next() else { + return agg; + }; + let value = id.value(); + let Some(value) = value.as_u64() else { + return agg; + }; + agg.push(value); + agg + }); + + Ok(ids) +} + #[derive(Debug, Message)] #[rtype(result = "Result<(), TantivyError>")] pub struct Refresh { @@ -177,33 +175,88 @@ pub struct Refresh { } impl Handler for SearchEngine { - type Result = actix::ResponseActFuture>; + type Result = Result<(), TantivyError>; fn handle(&mut self, msg: Refresh, _ctx: &mut Self::Context) -> Self::Result { - let inner = self.0.clone(); - Box::pin( - async move { - let mut shared = inner.lock().unwrap(); - - let id = shared.schema.get_field("id").unwrap(); - let title = shared.schema.get_field("summary").unwrap(); - let summary = shared.schema.get_field("summary").unwrap(); - - shared.writer.delete_all_documents()?; - - for msg in msg.records { - tracing::debug!("creating search index for {msg:?}"); - let _n = shared.writer.add_document(doc! { - id => msg.id, - title => msg.title, - summary => msg.summary.unwrap_or_default(), - })?; - } - shared.writer.commit()?; - - Ok(()) - } - .into_actor(self), - ) + refresh(self.0.clone(), msg) + } +} + +fn refresh(inner: Arc>, msg: Refresh) -> Result<(), TantivyError> { + let mut shared = inner.lock().unwrap(); + + let id = shared.schema.get_field(ID).unwrap(); + let title = shared.schema.get_field(TITLE).unwrap(); + let summary = shared.schema.get_field(SUMMARY).unwrap(); + + shared.writer.delete_all_documents()?; + + for msg in msg.records { + tracing::debug!("creating search index for {msg:?}"); + let n = shared.writer.add_document(doc! { + id => msg.id, + title => msg.title, + summary => msg.summary.unwrap_or_default(), + })?; + tracing::debug!("add_document {n}"); + } + shared.writer.commit()?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use tempfile::tempdir; + use tracing_test::traced_test; + + use super::*; + + #[traced_test] + #[test] + fn find_doc() { + let dir = tempdir().unwrap(); + + let s = SearchEngine::build(dir.path()).unwrap(); + refresh( + s.0.clone(), + Refresh { + records: vec![ + RecipeRecord { + id: 1, + title: "Crab".into(), + summary: Some("One".into()), + }, + RecipeRecord { + id: 1, + title: "Cream".into(), + summary: Some("One".into()), + }, + RecipeRecord { + id: 1, + title: "Wine".into(), + summary: Some("One".into()), + }, + ], + }, + ) + .unwrap(); + + let v = find( + s.0.clone(), + Find { + query: "Crab".into(), + }, + ) + .unwrap(); + assert_eq!(v.len(), 1); + let v = find( + s.0.clone(), + Find { + query: "Wine".into(), + }, + ) + .unwrap(); + assert_eq!(v.len(), 1); } } diff --git a/src/main.rs b/src/main.rs index e5cb35c..a8b4c4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use actix::Actor; +use actix::SyncArbiter; use actix_files::Files; use actix_identity::IdentityMiddleware; use actix_session::{storage::RedisSessionStore, SessionMiddleware}; @@ -84,9 +84,9 @@ async fn main() { let redis_store = RedisSessionStore::new(redis_url.as_str()).await.unwrap(); let search = { - let search_addr = crate::actors::search::SearchEngine::build() - .unwrap() - .start(); + let search_addr = SyncArbiter::start(4, move || { + crate::actors::search::SearchEngine::build(&std::path::Path::new("./indices")).unwrap() + }); use crate::actors::search::*; use sea_orm::prelude::*; let records = entities::prelude::Recipies::find() diff --git a/src/routes.rs b/src/routes.rs index 74a45ea..bccaf48 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,6 +1,8 @@ +use crate::actors::search::{Find, Search}; use crate::types::*; use crate::{entities, filters}; use actix_identity::Identity; +use actix_web::http::header::{CONTENT_LENGTH, CONTENT_TYPE}; use actix_web::web::{Data, Form, Path}; use actix_web::HttpMessage; use actix_web::{get, post, HttpRequest, HttpResponse, Responder}; @@ -9,11 +11,11 @@ use sea_orm::{prelude::*, QuerySelect}; use serde::Deserialize; #[derive(Debug, Template, derive_more::Deref)] -#[template(path = "recipe_card.html")] +#[template(path = "recipe_card.jinja")] struct RecipeCard(entities::recipies::Model); #[derive(Debug, Template)] -#[template(path = "index.html")] +#[template(path = "index.jinja")] struct IndexTemplate { recipies: Vec, count: u64, @@ -22,7 +24,7 @@ struct IndexTemplate { } #[derive(Debug, Template)] -#[template(path = "search.html")] +#[template(path = "search.jinja")] struct SearchTemplate { recipies: Vec, count: u64, @@ -32,7 +34,7 @@ struct SearchTemplate { } #[derive(Debug, Template)] -#[template(path = "top_bar.html")] +#[template(path = "top_bar.jinja")] struct TopBar<'s> { session: &'s Option, } @@ -44,7 +46,7 @@ impl<'s> TopBar<'s> { } #[derive(Debug, Template)] -#[template(path = "sign_in/form.html")] +#[template(path = "sign_in/form.jinja")] struct SignInForm { not_found: bool, email: String, @@ -139,14 +141,34 @@ struct SearchQuery { #[post("/search")] async fn search_results( - q: actix_web::web::Query, + q: actix_web::web::Form, admin: Option, + search: Data, + db: Data, ) -> SearchTemplate { let query = q.into_inner().q; + let ids = match search + .send(Find { + query: query.clone(), + }) + .await + { + Ok(Ok(ids)) => ids, + _ => vec![], + }; + let recipies = entities::prelude::Recipies::find() + .filter(entities::recipies::Column::Id.is_in(ids)) + .all(&**db) + .await + .unwrap_or_default() + .into_iter() + .map(RecipeCard) + .collect::>(); + SearchTemplate { - query: "".into(), - recipies: Vec::new(), - count: 0, + query: query.into(), + count: recipies.len() as u64, + recipies, session: admin.and_then(|a| a.id().ok()), page: Page::Search, } @@ -157,7 +179,7 @@ async fn index_html( db: Data, q: actix_web::web::Query, admin: Option, -) -> IndexTemplate { +) -> HttpResponse { let count = (entities::prelude::Recipies::find() .count(&**db) .await @@ -171,16 +193,23 @@ async fn index_html( .await .unwrap_or_default(); - IndexTemplate { + let html = IndexTemplate { recipies: recipies.into_iter().map(RecipeCard).collect(), count, session: admin.and_then(|s| s.id().ok()), page: Page::Index, } + .render() + .unwrap(); + + HttpResponse::Ok() + .append_header((CONTENT_TYPE, "text/html; charset=utf-8")) + .append_header((CONTENT_LENGTH, html.len())) + .body(html) } #[derive(Debug, Template)] -#[template(path = "recipies/show.html")] +#[template(path = "recipies/show.jinja")] struct RecipeDetailTemplate { recipe: entities::recipies::Model, tags: Vec, diff --git a/tailwind.config.js b/tailwind.config.js index 74f21b8..6c6ae3d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -43,7 +43,7 @@ ], daisyui: { themes: ['corporate', 'dark'], // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"] - darkTheme: "corporate", // name of one of the included themes for dark mode + darkTheme: "dark", // name of one of the included themes for dark mode base: true, // applies background color and foreground color for root element by default styled: true, // include daisyUI colors and design decisions for all components utils: true, // adds responsive and modifier utility classes diff --git a/templates/base.html b/templates/base.jinja similarity index 87% rename from templates/base.html rename to templates/base.jinja index 2fa5d51..9cfebb9 100644 --- a/templates/base.html +++ b/templates/base.jinja @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@
{% block navigation %}{% endblock %} - {% include "./nav.html" %} + {% include "./nav.jinja" %} {% block content %}{% endblock %}
diff --git a/templates/index.html b/templates/index.jinja similarity index 96% rename from templates/index.html rename to templates/index.jinja index 36aedf4..9f31bbe 100644 --- a/templates/index.html +++ b/templates/index.jinja @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "base.jinja" %} {% block content %}
diff --git a/templates/nav.html b/templates/nav.html deleted file mode 100644 index 78dee99..0000000 --- a/templates/nav.html +++ /dev/null @@ -1,37 +0,0 @@ -{% block navigation %} - -
- -
-{% endblock %} diff --git a/templates/nav.jinja b/templates/nav.jinja new file mode 100644 index 0000000..632d9ec --- /dev/null +++ b/templates/nav.jinja @@ -0,0 +1,80 @@ +{% block navigation %} + +
+ +
+{% endblock %} diff --git a/templates/recipe_card.html b/templates/recipe_card.jinja similarity index 100% rename from templates/recipe_card.html rename to templates/recipe_card.jinja diff --git a/templates/recipies/create_form.jinja b/templates/recipies/create_form.jinja new file mode 100644 index 0000000..dfeae94 --- /dev/null +++ b/templates/recipies/create_form.jinja @@ -0,0 +1,38 @@ +{% extends "base.jinja" %} + +{% block content %} +
+ {{ TopBar::new(session)|safe }} + +
+

Create recipe

+
+
+
+
+ +