Add search frontend code
This commit is contained in:
parent
988849e598
commit
3e91f34303
22 changed files with 379 additions and 73 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
|
@ -100,12 +100,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "anstyle-wincon"
|
||||||
version = "3.0.7"
|
version = "3.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell",
|
"once_cell_polyfill",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -499,9 +499,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.23"
|
version = "1.2.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
|
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
@ -2011,12 +2011,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onig"
|
name = "once_cell_polyfill"
|
||||||
version = "6.4.0"
|
version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
|
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onig"
|
||||||
|
version = "6.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 2.9.1",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"onig_sys",
|
"onig_sys",
|
||||||
|
|
@ -2024,9 +2030,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onig_sys"
|
name = "onig_sys"
|
||||||
version = "69.8.1"
|
version = "69.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
|
checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
|
|
@ -2628,9 +2634,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.20"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
.svg-arrow {
|
.svg-arrow {
|
||||||
display: block;
|
display: block;
|
||||||
margin: consts.$margin calc(consts.$margin / 2);
|
margin: var(--margin) calc(var(--margin) / 2);
|
||||||
fill: consts.$link-color;
|
fill: consts.$link-color;
|
||||||
width: 0.8rem;
|
width: 0.8rem;
|
||||||
height: 0.8rem;
|
height: 0.8rem;
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.year {
|
.year {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.month {
|
.month {
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-scheduled-recipe {
|
.remove-scheduled-recipe {
|
||||||
padding: 0px calc(consts.$margin / 2);
|
padding: 0px calc(var(--margin) / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,25 @@
|
||||||
|
|
||||||
$dark-theme: false !default;
|
$dark-theme: false !default;
|
||||||
|
|
||||||
|
$mobile-width: 1080px;
|
||||||
|
|
||||||
|
// Dynamic values depending of the screen width.
|
||||||
|
@media (min-width: calc($mobile-width + 1px)) {
|
||||||
|
:root {
|
||||||
|
--margin: 5px;
|
||||||
|
--title-font-size: 180%;
|
||||||
|
--logo-size: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
:root {
|
||||||
|
--margin: 3px;
|
||||||
|
--title-font-size: 130%;
|
||||||
|
--logo-size: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$color-1: #B29B89;
|
$color-1: #B29B89;
|
||||||
$color-2: #89B29B;
|
$color-2: #89B29B;
|
||||||
$color-3: #9B89B2;
|
$color-3: #9B89B2;
|
||||||
|
|
@ -9,21 +28,22 @@ $color-highlight: #cf2d2dff;
|
||||||
|
|
||||||
$text-color: color.adjust($color-1, $lightness: -30%);
|
$text-color: color.adjust($color-1, $lightness: -30%);
|
||||||
$text-highlight: color.adjust($color-1, $lightness: +30%);
|
$text-highlight: color.adjust($color-1, $lightness: +30%);
|
||||||
|
$text-dimished: color.adjust($color-1, $lightness: -15%, $saturation: -15%);
|
||||||
|
|
||||||
$link-color: color.adjust($color-3, $lightness: -25%);
|
$link-color: color.adjust($color-3, $lightness: -25%);
|
||||||
$link-hover-color: color.adjust($color-3, $lightness: +20%);
|
$link-hover-color: color.adjust($color-3, $lightness: +20%);
|
||||||
|
|
||||||
@if $dark-theme {
|
@if $dark-theme {
|
||||||
|
$color-highlight: color.adjust($color-highlight, $lightness: +10%);
|
||||||
|
|
||||||
$text-color: color.adjust($color-1, $lightness: -10%);
|
$text-color: color.adjust($color-1, $lightness: -10%);
|
||||||
$text-highlight: color.adjust($color-1, $lightness: +10%);
|
$text-highlight: color.adjust($color-1, $lightness: +10%);
|
||||||
$color-highlight: color.adjust($color-highlight, $lightness: +10%);
|
$text-dimished: color.adjust($color-1, $lightness: -20%, $saturation: -15%);
|
||||||
|
|
||||||
$link-color: color.adjust($color-3, $lightness: -5%);
|
$link-color: color.adjust($color-3, $lightness: -5%);
|
||||||
$link-hover-color: color.adjust($color-3, $lightness: +10%);
|
$link-hover-color: color.adjust($color-3, $lightness: +10%);
|
||||||
|
|
||||||
$color-1: color.adjust($color-1, $lightness: -47%);
|
$color-1: color.adjust($color-1, $lightness: -47%);
|
||||||
$color-2: color.adjust($color-2, $lightness: -47%);
|
$color-2: color.adjust($color-2, $lightness: -47%);
|
||||||
$color-3: color.adjust($color-2, $lightness: -47%);
|
$color-3: color.adjust($color-3, $lightness: -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
$margin: 5px;
|
|
||||||
|
|
@ -4,6 +4,7 @@ $dark-theme: false !default;
|
||||||
|
|
||||||
@use 'constants' as consts with ($dark-theme: $dark-theme);
|
@use 'constants' as consts with ($dark-theme: $dark-theme);
|
||||||
@use 'calendar';
|
@use 'calendar';
|
||||||
|
@use 'search';
|
||||||
@use 'toast';
|
@use 'toast';
|
||||||
@use 'modal-dialog';
|
@use 'modal-dialog';
|
||||||
@use 'mixins';
|
@use 'mixins';
|
||||||
|
|
@ -40,26 +41,26 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-container {
|
.footer-container {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HEADER ///
|
/// HEADER ///
|
||||||
.header-container {
|
.header-container {
|
||||||
margin: calc(2 * consts.$margin);
|
margin: calc(2 * var(--margin));
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 180%;
|
font-size: var(--title-font-size);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 50px;
|
width: var(--logo-size);
|
||||||
height: 50px;
|
height: var(--logo-size);
|
||||||
|
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
margin: 0px 10px 0px 0px;
|
margin: 0px 10px 0px 0px;
|
||||||
|
|
@ -68,16 +69,16 @@ body {
|
||||||
|
|
||||||
.header-menu {
|
.header-menu {
|
||||||
.user-menu {
|
.user-menu {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
|
|
||||||
.user-edit-link {
|
.user-edit-link {
|
||||||
margin-left: consts.$margin;
|
margin-left: var(--margin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#select-website-language {
|
#select-website-language {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
padding: consts.$margin;
|
padding: var(--margin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,16 +88,16 @@ body {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
#recipes-list {
|
#recipes-list {
|
||||||
margin: calc(2 * consts.$margin);
|
margin: calc(2 * var(--margin));
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-bottom: calc(consts.$margin / 2);
|
margin-bottom: calc(var(--margin) / 2);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
padding: calc(consts.$margin / 2);
|
padding: calc(var(--margin) / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +131,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: calc(2 * consts.$margin);
|
margin: calc(2 * var(--margin));
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
|
@ -250,13 +251,11 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #dev-panel {
|
|
||||||
// }
|
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
// <label> <input>.
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
gap: calc(consts.$margin / 2);
|
gap: calc(var(--margin) / 2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,14 +278,14 @@ body {
|
||||||
border: 0.1em solid consts.$color-3;
|
border: 0.1em solid consts.$color-3;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
background-color: consts.$color-1;
|
background-color: consts.$color-1;
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
padding: consts.$margin;
|
padding: var(--margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea,
|
textarea,
|
||||||
input {
|
input {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
padding: calc(consts.$margin / 2) calc(2 * consts.$margin);
|
padding: calc(var(--margin) / 2) calc(2 * var(--margin));
|
||||||
|
|
||||||
background-color: consts.$color-1;
|
background-color: consts.$color-1;
|
||||||
border: solid 1px consts.$color-3;
|
border: solid 1px consts.$color-3;
|
||||||
|
|
@ -300,8 +299,8 @@ input {
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
padding: calc(consts.$margin / 2) calc(2 * consts.$margin);
|
padding: calc(var(--margin) / 2) calc(2 * var(--margin));
|
||||||
|
|
||||||
background-color: consts.$color-1;
|
background-color: consts.$color-1;
|
||||||
border: solid 1px consts.$color-3;
|
border: solid 1px consts.$color-3;
|
||||||
|
|
@ -316,8 +315,8 @@ select {
|
||||||
input[type="button"],
|
input[type="button"],
|
||||||
input[type="submit"],
|
input[type="submit"],
|
||||||
.button {
|
.button {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
padding: calc(consts.$margin / 2) calc(2 * consts.$margin);
|
padding: calc(var(--margin) / 2) calc(2 * var(--margin));
|
||||||
|
|
||||||
border: 0.1em solid consts.$color-3;
|
border: 0.1em solid consts.$color-3;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
|
|
@ -345,19 +344,19 @@ input[type="submit"],
|
||||||
width: 120px;
|
width: 120px;
|
||||||
background-color: consts.$color-1;
|
background-color: consts.$color-1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: consts.$margin 0;
|
padding: var(--margin) 0;
|
||||||
|
|
||||||
border: 0.1em solid consts.$color-3;
|
border: 0.1em solid consts.$color-3;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 10;
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
top: -(consts.$margin);
|
top: -(var(--margin));
|
||||||
left: 100%;
|
left: 100%;
|
||||||
margin-left: consts.$margin;
|
margin-left: var(--margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .tooltiptext {
|
&:hover .tooltiptext {
|
||||||
|
|
@ -369,8 +368,8 @@ input[type="submit"],
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 100%;
|
right: 100%;
|
||||||
margin-top: -(consts.$margin);
|
margin-top: -(var(--margin));
|
||||||
border-width: consts.$margin;
|
border-width: var(--margin);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: transparent consts.$color-3 transparent transparent;
|
border-color: transparent consts.$color-3 transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +377,7 @@ input[type="submit"],
|
||||||
|
|
||||||
/// Toggle theme (dark, light).
|
/// Toggle theme (dark, light).
|
||||||
#toggle-theme {
|
#toggle-theme {
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,6 @@
|
||||||
|
|
||||||
color: consts.$text-color;
|
color: consts.$text-color;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: calc(2 * consts.$margin) calc(2 * consts.$margin);
|
padding: calc(2 * var(--margin)) calc(2 * var(--margin));
|
||||||
box-shadow: -1px 1px 10px rgba(0, 0, 0, 0.3);
|
box-shadow: -1px 1px 10px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
#modal-dialog {
|
#modal-dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 5;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
transform: translate(-50%, 0%);
|
transform: translate(-50%, 0%);
|
||||||
|
|
@ -16,6 +16,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
padding: calc(2 * consts.$margin) calc(2 * consts.$margin);
|
padding: calc(2 * var(--margin)) calc(2 * var(--margin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
73
backend/scss/search.scss
Normal file
73
backend/scss/search.scss
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
@use 'sass:color';
|
||||||
|
|
||||||
|
@use 'constants' as consts;
|
||||||
|
|
||||||
|
#search {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
line-height: 28px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: consts.$color-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: consts.$text-dimished;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
fill: consts.$color-3;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
display: none;
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
vertical-align: top;
|
||||||
|
top: 100%;
|
||||||
|
|
||||||
|
border: solid 1px consts.$color-3;
|
||||||
|
background-color: consts.$color-1;
|
||||||
|
box-shadow: 0 0 5px color.adjust(consts.$color-3, $lightness: -20%);
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: hidden;
|
||||||
|
padding:
|
||||||
|
var(--margin) calc(2 * var(--margin)) var(--margin) calc(2 * var(--margin));
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding:
|
||||||
|
var(--margin) calc(2 * var(--margin)) var(--margin) calc(2 * var(--margin));
|
||||||
|
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li:nth-child(odd) {
|
||||||
|
background: consts.$color-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#hidden-templates-search {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
#toasts {
|
#toasts {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 6;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
transform: translate(-50%, 0%);
|
transform: translate(-50%, 0%);
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
@include mixins.popup;
|
@include mixins.popup;
|
||||||
|
|
||||||
margin: consts.$margin;
|
margin: var(--margin);
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,13 @@ CREATE TABLE [Recipe] (
|
||||||
CREATE VIRTUAL TABLE [RecipeTitle] USING FTS5(
|
CREATE VIRTUAL TABLE [RecipeTitle] USING FTS5(
|
||||||
[title],
|
[title],
|
||||||
CONTENT = [Recipe],
|
CONTENT = [Recipe],
|
||||||
CONTENT_ROWID = [id]
|
CONTENT_ROWID = [id],
|
||||||
|
PREFIX = 2,
|
||||||
|
PREFIX = 3,
|
||||||
|
PREFIX = 4,
|
||||||
|
PREFIX = 5,
|
||||||
|
PREFIX = 6,
|
||||||
|
PREFIX = 7,
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER [Recipe_trigger_insert] AFTER INSERT ON [Recipe] BEGIN
|
CREATE TRIGGER [Recipe_trigger_insert] AFTER INSERT ON [Recipe] BEGIN
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use std::u32;
|
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use common::web_api::Difficulty;
|
use common::web_api::Difficulty;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
@ -287,18 +285,21 @@ FROM [Recipe] WHERE [id] = $1
|
||||||
&self,
|
&self,
|
||||||
lang: &str,
|
lang: &str,
|
||||||
term: &str,
|
term: &str,
|
||||||
|
max_nb_results: u32,
|
||||||
) -> Result<Vec<model::RecipeSearchResult>> {
|
) -> Result<Vec<model::RecipeSearchResult>> {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT [id], [recipe].[title], highlight([RecipeTitle], 0, '<em>', '</em>') [title_highlighted]
|
SELECT [id], [recipe].[title], highlight([RecipeTitle], 0, '<em>', '</em>') [title_highlighted]
|
||||||
FROM [RecipeTitle]
|
FROM [RecipeTitle]
|
||||||
INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTitle].[rowid]
|
INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTitle].[rowid]
|
||||||
WHERE [Recipe].[lang] = $1 AND [RecipeTitle] MATCH $2
|
WHERE [Recipe].[is_public] AND [Recipe].[lang] = $1 AND [RecipeTitle] MATCH $2
|
||||||
ORDER BY RANK, [Recipe].[title]
|
ORDER BY RANK, [Recipe].[title]
|
||||||
|
LIMIT $3
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(lang)
|
.bind(lang)
|
||||||
.bind(term)
|
.bind(term)
|
||||||
|
.bind(max_nb_results)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
|
|
@ -964,6 +965,8 @@ pub fn normalize_tag(tag: &str) -> String {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::u32;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::Days;
|
use chrono::Days;
|
||||||
|
|
||||||
|
|
@ -1011,7 +1014,7 @@ mod tests {
|
||||||
let id3 = add_recipe(&connection, user_id, "AAA ZZZ").await?;
|
let id3 = add_recipe(&connection, user_id, "AAA ZZZ").await?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let recipes = connection.search_recipes("en", "yyy").await?;
|
let recipes = connection.search_recipes("en", "yyy", u32::MAX).await?;
|
||||||
|
|
||||||
assert_eq!(recipes.len(), 2);
|
assert_eq!(recipes.len(), 2);
|
||||||
|
|
||||||
|
|
@ -1031,7 +1034,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let recipes = connection.search_recipes("en", "aaa OR zzz").await?;
|
let recipes = connection
|
||||||
|
.search_recipes("en", "aaa OR zzz", u32::MAX)
|
||||||
|
.await?;
|
||||||
|
|
||||||
assert_eq!(recipes.len(), 3);
|
assert_eq!(recipes.len(), 3);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Query;
|
use axum_extra::extract::Query;
|
||||||
use common::web_api;
|
use common::web_api;
|
||||||
|
use itertools::Itertools;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
|
|
@ -19,13 +20,35 @@ pub async fn search_by_title(
|
||||||
Extension(context): Extension<Context>,
|
Extension(context): Extension<Context>,
|
||||||
Query(params): Query<web_api::SearchByTitleParams>,
|
Query(params): Query<web_api::SearchByTitleParams>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
|
let trimmed_search_term = params.search_term.trim();
|
||||||
|
|
||||||
Ok(ron_response_ok(
|
Ok(ron_response_ok(
|
||||||
connection
|
if trimmed_search_term.len() < common::consts::MIN_SEARCH_STRING_LENGTH {
|
||||||
.search_recipes(context.tr.current_lang_code(), ¶ms.search_term)
|
// Avoid searching 1 or 2 characters.
|
||||||
.await?
|
vec![]
|
||||||
.into_iter()
|
} else {
|
||||||
.map(web_api::RecipeSearchResult::from)
|
let mut forced_prefix_words = trimmed_search_term
|
||||||
.collect::<Vec<_>>(),
|
.split(' ')
|
||||||
|
.filter(|word| word.len() >= common::consts::MIN_SEARCH_STRING_LENGTH)
|
||||||
|
.join("* OR ");
|
||||||
|
forced_prefix_words.push('*');
|
||||||
|
|
||||||
|
match connection
|
||||||
|
.search_recipes(
|
||||||
|
context.tr.current_lang_code(),
|
||||||
|
&forced_prefix_words,
|
||||||
|
params.max_nb_results,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => result
|
||||||
|
.into_iter()
|
||||||
|
.map(web_api::RecipeSearchResult::from)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
|
||||||
|
Err(_) => vec![],
|
||||||
|
}
|
||||||
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
{% include "title.html" %}
|
{% include "title.html" %}
|
||||||
|
|
||||||
<span class="header-menu">
|
<span class="header-menu">
|
||||||
|
{% include "search.html" %}
|
||||||
|
|
||||||
<span class="user-menu">
|
<span class="user-menu">
|
||||||
{% match context.user %}
|
{% match context.user %}
|
||||||
{% when Some with (user) %}
|
{% when Some with (user) %}
|
||||||
|
|
|
||||||
17
backend/templates/search.html
Normal file
17
backend/templates/search.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<span id="search">
|
||||||
|
<svg class="icon" aria-hidden="true" viewBox="0 0 24 24"><g><path d="M21.53 20.47l-3.66-3.66C19.195 15.24 20 13.214 20 11c0-4.97-4.03-9-9-9s-9 4.03-9 9 4.03 9 9 9c2.215 0 4.24-.804 5.808-2.13l3.66 3.66c.147.146.34.22.53.22s.385-.073.53-.22c.295-.293.295-.767.002-1.06zM3.5 11c0-4.135 3.365-7.5 7.5-7.5s7.5 3.365 7.5 7.5-3.365 7.5-7.5 7.5-7.5-3.365-7.5-7.5z"></path></g></svg>
|
||||||
|
<input type="search" placeholder="{{ context.tr.t(Sentence::SearchPlaceholder) }}" />
|
||||||
|
|
||||||
|
<div class="results">
|
||||||
|
<div class="message">
|
||||||
|
<span class="to-small">{{ context.tr.t(Sentence::SearchTooFewCharacters) }}</span>
|
||||||
|
<span class="no-results">{{ context.tr.t(Sentence::SearchNoResults) }}</span>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div id="hidden-templates-search">
|
||||||
|
<li class="result-item"><a></a></li>
|
||||||
|
</div>
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
(TemplateError, "Template error"),
|
(TemplateError, "Template error"),
|
||||||
|
|
||||||
(SearchPlaceholder, "Search by title"),
|
(SearchPlaceholder, "Search by title"),
|
||||||
|
(SearchTooFewCharacters, "Too few characters"),
|
||||||
|
(SearchNoResults, "No results"),
|
||||||
|
|
||||||
(SignInMenu, "Sign in"),
|
(SignInMenu, "Sign in"),
|
||||||
(SignInTitle, "Sign in"),
|
(SignInTitle, "Sign in"),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
(TemplateError, "Erreur du moteur de modèles (Template error)"),
|
(TemplateError, "Erreur du moteur de modèles (Template error)"),
|
||||||
|
|
||||||
(SearchPlaceholder, "Recherche par titre"),
|
(SearchPlaceholder, "Recherche par titre"),
|
||||||
|
(SearchTooFewCharacters, "Trop peu de caractères"),
|
||||||
|
(SearchNoResults, "Pas de résultats"),
|
||||||
|
|
||||||
(SignInMenu, "Se connecter"),
|
(SignInMenu, "Se connecter"),
|
||||||
(SignInTitle, "Se connecter"),
|
(SignInTitle, "Se connecter"),
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,5 @@ pub const GET_PARAMETER_USER_MESSAGE_LEVEL: &str = "user_message_icon";
|
||||||
// It should be "application/ron" but firefox can't display the response propertly (base64)
|
// It should be "application/ron" but firefox can't display the response propertly (base64)
|
||||||
// in the dev tool.
|
// in the dev tool.
|
||||||
pub const MIME_TYPE_RON: HeaderValue = HeaderValue::from_static("text/ron");
|
pub const MIME_TYPE_RON: HeaderValue = HeaderValue::from_static("text/ron");
|
||||||
|
|
||||||
|
pub const MIN_SEARCH_STRING_LENGTH: usize = 3;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ pub enum Sentence {
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
SearchPlaceholder,
|
SearchPlaceholder,
|
||||||
|
SearchTooFewCharacters,
|
||||||
|
SearchNoResults,
|
||||||
|
|
||||||
// Sign in page.
|
// Sign in page.
|
||||||
SignInMenu,
|
SignInMenu,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub struct RecipesListFragmentsParams {
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct SearchByTitleParams {
|
pub struct SearchByTitleParams {
|
||||||
pub search_term: String,
|
pub search_term: String,
|
||||||
|
pub max_nb_results: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ web-sys = { version = "0.3", features = [
|
||||||
"HtmlElement",
|
"HtmlElement",
|
||||||
"MediaQueryList",
|
"MediaQueryList",
|
||||||
"HtmlDivElement",
|
"HtmlDivElement",
|
||||||
|
"HtmlSpanElement",
|
||||||
"HtmlLabelElement",
|
"HtmlLabelElement",
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
"HtmlImageElement",
|
"HtmlImageElement",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ mod pages;
|
||||||
mod recipe_scheduler;
|
mod recipe_scheduler;
|
||||||
mod request;
|
mod request;
|
||||||
mod ron_request;
|
mod ron_request;
|
||||||
|
mod search;
|
||||||
mod shopping_list;
|
mod shopping_list;
|
||||||
mod toast;
|
mod toast;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
@ -140,6 +141,9 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
})
|
})
|
||||||
.forget();
|
.forget();
|
||||||
|
|
||||||
|
// Search box.
|
||||||
|
search::setup();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
use std::{cell::RefCell, rc, sync::Mutex};
|
use std::{cell::RefCell, rc, sync::Mutex};
|
||||||
|
|
||||||
use common::{web_api, utils::substitute};
|
use common::{utils::substitute, web_api};
|
||||||
use gloo::{
|
use gloo::{
|
||||||
events::{EventListener, EventListenerOptions},
|
events::{EventListener, EventListenerOptions},
|
||||||
net::http::Request,
|
|
||||||
utils::{document, window},
|
utils::{document, window},
|
||||||
};
|
};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
@ -15,7 +14,6 @@ use web_sys::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
modal_dialog, request, ron_request,
|
modal_dialog, request, ron_request,
|
||||||
toast::{self, Level},
|
|
||||||
utils::{SelectorExt, by_id, get_current_lang, selector, selector_and_clone},
|
utils::{SelectorExt, by_id, get_current_lang, selector, selector_and_clone},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
143
frontend/src/search.rs
Normal file
143
frontend/src/search.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
use common::web_api;
|
||||||
|
use futures::{
|
||||||
|
StreamExt,
|
||||||
|
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
|
||||||
|
};
|
||||||
|
use gloo::{events::EventListener, utils::document};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use web_sys::{Element, HtmlDivElement, HtmlInputElement, HtmlSpanElement, KeyboardEvent};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ron_request,
|
||||||
|
utils::{SelectorExt, get_current_lang, selector, selector_and_clone},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_NB_SEARCH_RESULT: u32 = 10;
|
||||||
|
|
||||||
|
pub fn setup() {
|
||||||
|
// Here we check if the search input exists.
|
||||||
|
// Otherwise the search elements are considered as not present.
|
||||||
|
let input_search = match document().query_selector("#search input") {
|
||||||
|
Ok(Some(e)) => e.dyn_into::<HtmlInputElement>().unwrap(),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (sender, mut receiver): (UnboundedSender<String>, UnboundedReceiver<String>) = unbounded();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
while let Some(search_term) = receiver.next().await {
|
||||||
|
perform_search(search_term).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
EventListener::new(&input_search, "input", move |event| {
|
||||||
|
let input_search: HtmlInputElement = event.target().unwrap().dyn_into().unwrap();
|
||||||
|
let search_term = input_search.value().trim().to_string();
|
||||||
|
sender.unbounded_send(search_term).unwrap();
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
EventListener::new(&input_search, "keydown", move |event| {
|
||||||
|
let key_event: &KeyboardEvent = event.dyn_ref().unwrap();
|
||||||
|
if key_event.key() == "Escape" {
|
||||||
|
let input_search: HtmlInputElement = key_event.target().unwrap().dyn_into().unwrap();
|
||||||
|
input_search.set_value("");
|
||||||
|
selector::<HtmlDivElement>("#search .results")
|
||||||
|
.style()
|
||||||
|
.set_property("display", "none")
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn perform_search(search_term: String) {
|
||||||
|
let results_element: HtmlDivElement = selector("#search .results");
|
||||||
|
|
||||||
|
if search_term.is_empty() {
|
||||||
|
results_element
|
||||||
|
.style()
|
||||||
|
.set_property("display", "none")
|
||||||
|
.unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results_element
|
||||||
|
.selector::<Element>("ul")
|
||||||
|
.set_text_content(None);
|
||||||
|
|
||||||
|
if search_term.len() < common::consts::MIN_SEARCH_STRING_LENGTH {
|
||||||
|
display_message(Message::TooFewCharacters);
|
||||||
|
} else {
|
||||||
|
display_message(Message::None);
|
||||||
|
let search_result: Vec<web_api::RecipeSearchResult> = ron_request::get_with_params(
|
||||||
|
"/ron-api/recipe/search",
|
||||||
|
web_api::SearchByTitleParams {
|
||||||
|
search_term,
|
||||||
|
max_nb_results: MAX_NB_SEARCH_RESULT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if search_result.is_empty() {
|
||||||
|
display_message(Message::NoResults);
|
||||||
|
} else {
|
||||||
|
let ul_element = results_element.selector::<Element>("ul");
|
||||||
|
for recipe in search_result {
|
||||||
|
let li_element: Element = selector_and_clone("#hidden-templates-search li");
|
||||||
|
let a_element = li_element.selector::<Element>("a");
|
||||||
|
a_element
|
||||||
|
.set_attribute(
|
||||||
|
"href",
|
||||||
|
&format!("/{}/recipe/view/{}", get_current_lang(), recipe.recipe_id),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
a_element.set_inner_html(&recipe.title_highlighted);
|
||||||
|
ul_element.append_child(&li_element).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results_element
|
||||||
|
.style()
|
||||||
|
.set_property("display", "block")
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Message {
|
||||||
|
None,
|
||||||
|
TooFewCharacters,
|
||||||
|
NoResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_message(message: Message) {
|
||||||
|
let message_element: HtmlDivElement = selector("#search .message");
|
||||||
|
|
||||||
|
for m in message_element.selector_all::<HtmlSpanElement>("span") {
|
||||||
|
m.style().set_property("display", "none").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
message_element
|
||||||
|
.style()
|
||||||
|
.set_property("display", "block")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Message::TooFewCharacters => message_element
|
||||||
|
.selector::<HtmlSpanElement>(".to-small")
|
||||||
|
.style()
|
||||||
|
.set_property("display", "inline")
|
||||||
|
.unwrap(),
|
||||||
|
Message::NoResults => message_element
|
||||||
|
.selector::<HtmlSpanElement>(".no-results")
|
||||||
|
.style()
|
||||||
|
.set_property("display", "inline")
|
||||||
|
.unwrap(),
|
||||||
|
Message::None => message_element
|
||||||
|
.style()
|
||||||
|
.set_property("display", "none")
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue