diff --git a/.gitignore b/.gitignore index ea8c4bf..bcd5559 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +.DS_Store diff --git a/BookForge.toml b/BookForge.toml index 3c2268e..0ac7cc8 100644 --- a/BookForge.toml +++ b/BookForge.toml @@ -1,4 +1,6 @@ database_path = "" +locale = "fr" +base_url = "" [listener] port = 8000 diff --git a/Cargo.lock b/Cargo.lock index 232c525..ef09c66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "arc-swap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -287,6 +296,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "base62" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111" + [[package]] name = "base64" version = "0.22.1" @@ -322,6 +337,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -360,9 +381,11 @@ dependencies = [ "askama_web", "axum", "camino", + "csv", "dirs", "log", "pretty_env_logger", + "rust-i18n", "sea-orm", "sea-orm-migration", "serde", @@ -370,7 +393,7 @@ dependencies = [ "snafu", "static-serve", "tokio", - "toml", + "toml 0.9.11+spec-1.1.0", "xdg", ] @@ -397,6 +420,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -582,6 +615,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -607,6 +659,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "darling" version = "0.20.11" @@ -996,6 +1069,30 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1302,6 +1399,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1362,6 +1475,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1424,7 +1546,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", "redox_syscall 0.7.0", ] @@ -1529,6 +1651,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "normpath" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1784,7 +1915,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -1908,7 +2039,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -1917,7 +2048,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -2038,6 +2169,60 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-i18n" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yaml", + "syn 2.0.114", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "toml 0.8.23", + "triomphe", +] + [[package]] name = "rust_decimal" version = "1.40.0" @@ -2081,6 +2266,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schemars" version = "0.9.0" @@ -2136,7 +2330,7 @@ dependencies = [ "chrono", "derive_more", "futures-util", - "itertools", + "itertools 0.14.0", "log", "ouroboros", "pgvector", @@ -2340,6 +2534,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -2392,6 +2595,19 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2451,6 +2667,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.11" @@ -2613,7 +2835,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -2659,7 +2881,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -2963,6 +3185,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + [[package]] name = "toml" version = "0.9.11+spec-1.1.0" @@ -2971,13 +3205,22 @@ checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ "indexmap 2.13.0", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -2987,6 +3230,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" @@ -2994,7 +3251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.13.0", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] @@ -3008,6 +3265,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.6+spec-1.1.0" @@ -3089,6 +3352,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + [[package]] name = "typenum" version = "1.19.0" @@ -3134,6 +3408,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "url" version = "2.5.8" @@ -3181,6 +3461,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 145fe87..83d8600 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,5 @@ pretty_env_logger = "0.5.0" # custom logger log = "0.4.29" serde_with = "3.16.1" +csv = "1.4.0" +rust-i18n = "3.1.5" diff --git a/assets/css/fork-awesome.min.css b/assets/css/fork-awesome.min.css new file mode 100755 index 0000000..fd154dc --- /dev/null +++ b/assets/css/fork-awesome.min.css @@ -0,0 +1,12 @@ +/*! +Fork Awesome 1.2.0 +License - https://forkaweso.me/Fork-Awesome/license + +Copyright 2018 Dave Gandy & Fork Awesome + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */@font-face{font-family:ForkAwesome;src:url(../fonts/forkawesome-webfont.eot?v=1.2.0);src:url(../fonts/forkawesome-webfont.eot?#iefix&v=1.2.0) format('embedded-opentype'),url(../fonts/forkawesome-webfont.woff2?v=1.2.0) format('woff2'),url(../fonts/forkawesome-webfont.woff?v=1.2.0) format('woff'),url(../fonts/forkawesome-webfont.ttf?v=1.2.0) format('truetype'),url(../fonts/forkawesome-webfont.svg?v=1.2.0#forkawesomeregular) format('svg');font-weight:400;font-style:normal;font-display:block}.fa{display:inline-block;font:normal normal normal 14px/1 ForkAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-close:before,.fa-remove:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-repeat:before,.fa-rotate-right:before{content:"\f01e"}.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-exclamation-triangle:before,.fa-warning:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell-o:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-globe-e:before{content:"\f304"}.fa-globe-w:before{content:"\f305"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-community:before,.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-floppy-o:before,.fa-save:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus-g:before,.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-bolt:before,.fa-flash:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before,.fa-utensils:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-chain-broken:before,.fa-unlink:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:"\f150"}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:"\f151"}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:"\f152"}.fa-eur:before,.fa-euro:before{content:"\f153"}.fa-gbp:before,.fa-pound:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-inr:before,.fa-rupee:before{content:"\f156"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:"\f158"}.fa-krw:before,.fa-won:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-desc:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-desc:before,.fa-sort-amount-up:before{content:"\f161"}.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-desc:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-try:before,.fa-turkish-lira:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-bank:before,.fa-institution:before,.fa-university:before{content:"\f19c"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:"\f1c5"}.fa-file-archive-o:before,.fa-file-zip-o:before{content:"\f1c6"}.fa-file-audio-o:before,.fa-file-sound-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:"\f1d0"}.fa-empire:before,.fa-ge:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-paper-plane:before,.fa-send:before{content:"\f1d8"}.fa-paper-plane-o:before,.fa-send-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before,.fa-closed-captioning:before{content:"\f20a"}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before,.fa-gem:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-bed:before,.fa-hotel:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-medium-square:before{content:"\f2f8"}.fa-y-combinator:before,.fa-yc:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-television:before,.fa-tv:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo-v:before,.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:"\f2a3"}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-address-card:before,.fa-vcard:before{content:"\f2bb"}.fa-address-card-o:before,.fa-vcard-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.fa-mastodon:before{content:"\f2e1"}.fa-mastodon-alt:before{content:"\f2e2"}.fa-fork-awesome:before,.fa-fork-circle:before{content:"\f2e3"}.fa-peertube:before{content:"\f2e4"}.fa-diaspora:before{content:"\f2e5"}.fa-friendica:before{content:"\f2e6"}.fa-gnu-social:before{content:"\f2e7"}.fa-liberapay-square:before{content:"\f2e8"}.fa-liberapay:before{content:"\f2e9"}.fa-scuttlebutt:before,.fa-ssb:before{content:"\f2ea"}.fa-hubzilla:before{content:"\f2eb"}.fa-social-home:before{content:"\f2ec"}.fa-artstation:before{content:"\f2ed"}.fa-discord:before{content:"\f2ee"}.fa-discord-alt:before{content:"\f2ef"}.fa-patreon:before{content:"\f2f0"}.fa-snowdrift:before{content:"\f2f1"}.fa-activitypub:before{content:"\f2f2"}.fa-ethereum:before{content:"\f2f3"}.fa-keybase:before{content:"\f2f4"}.fa-shaarli:before{content:"\f2f5"}.fa-shaarli-o:before{content:"\f2f6"}.fa-cut-key:before,.fa-key-modern:before{content:"\f2f7"}.fa-xmpp:before{content:"\f2f9"}.fa-archive-org:before{content:"\f2fc"}.fa-freedombox:before{content:"\f2fd"}.fa-facebook-messenger:before{content:"\f2fe"}.fa-debian:before{content:"\f2ff"}.fa-mastodon-square:before{content:"\f300"}.fa-tipeee:before{content:"\f301"}.fa-react:before{content:"\f302"}.fa-dogmazic:before{content:"\f303"}.fa-zotero:before{content:"\f309"}.fa-nodejs:before{content:"\f308"}.fa-nextcloud:before{content:"\f306"}.fa-nextcloud-square:before{content:"\f307"}.fa-hackaday:before{content:"\f30a"}.fa-laravel:before{content:"\f30b"}.fa-signalapp:before{content:"\f30c"}.fa-gnupg:before{content:"\f30d"}.fa-php:before{content:"\f30e"}.fa-ffmpeg:before{content:"\f30f"}.fa-joplin:before{content:"\f310"}.fa-syncthing:before{content:"\f311"}.fa-inkscape:before{content:"\f312"}.fa-matrix-org:before{content:"\f313"}.fa-pixelfed:before{content:"\f314"}.fa-bootstrap:before{content:"\f315"}.fa-dev-to:before{content:"\f316"}.fa-hashnode:before{content:"\f317"}.fa-jirafeau:before{content:"\f318"}.fa-emby:before{content:"\f319"}.fa-wikidata:before{content:"\f31a"}.fa-gimp:before{content:"\f31b"}.fa-c:before{content:"\f31c"}.fa-digitalocean:before{content:"\f31d"}.fa-att:before{content:"\f31e"}.fa-gitea:before{content:"\f31f"}.fa-file-epub:before{content:"\f321"}.fa-python:before{content:"\f322"}.fa-archlinux:before{content:"\f323"}.fa-pleroma:before{content:"\f324"}.fa-unsplash:before{content:"\f325"}.fa-hackster:before{content:"\f326"}.fa-spell-check:before{content:"\f327"}.fa-moon:before{content:"\f328"}.fa-sun:before{content:"\f329"}.fa-f-droid:before{content:"\f32a"}.fa-biometric:before{content:"\f32b"}.fa-wire:before{content:"\f32c"}.fa-tor-onion:before{content:"\f32e"}.fa-volume-mute:before{content:"\f32f"}.fa-bell-ringing:before{content:"\f32d"}.fa-bell-ringing-o:before{content:"\f330"}.fa-hal:before{content:"\f333"}.fa-jupyter:before{content:"\f335"}.fa-julia:before{content:"\f334"}.fa-classicpress:before{content:"\f331"}.fa-classicpress-circle:before{content:"\f332"}.fa-open-collective:before{content:"\f336"}.fa-orcid:before{content:"\f337"}.fa-researchgate:before{content:"\f338"}.fa-funkwhale:before{content:"\f339"}.fa-askfm:before{content:"\f33a"}.fa-blockstack:before{content:"\f33b"}.fa-boardgamegeek:before{content:"\f33c"}.fa-bunny:before{content:"\f35f"}.fa-buymeacoffee:before{content:"\f33d"}.fa-cc-by:before{content:"\f33e"}.fa-cc-cc:before,.fa-creative-commons-alt:before{content:"\f33f"}.fa-cc-nc-eu:before{content:"\f341"}.fa-cc-nc-jp:before{content:"\f342"}.fa-cc-nc:before{content:"\f340"}.fa-cc-nd:before{content:"\f343"}.fa-cc-pd:before{content:"\f344"}.fa-cc-remix:before{content:"\f345"}.fa-cc-sa:before{content:"\f346"}.fa-cc-share:before{content:"\f347"}.fa-cc-zero:before{content:"\f348"}.fa-conway-glider:before,.fa-conway-hacker:before{content:"\f349"}.fa-csharp:before{content:"\f34a"}.fa-email-bulk:before{content:"\f34b"}.fa-email-bulk-o:before{content:"\f34c"}.fa-gnu:before{content:"\f34d"}.fa-google-play:before{content:"\f34e"}.fa-heroku:before{content:"\f34f"}.fa-hassio:before,.fa-home-assistant:before{content:"\f350"}.fa-java:before{content:"\f351"}.fa-mariadb:before{content:"\f352"}.fa-markdown:before{content:"\f353"}.fa-mysql:before{content:"\f354"}.fa-nordcast:before{content:"\f355"}.fa-plume:before{content:"\f356"}.fa-postgresql:before{content:"\f357"}.fa-sass-alt:before{content:"\f359"}.fa-sass:before{content:"\f358"}.fa-skate:before{content:"\f35a"}.fa-sketchfab:before{content:"\f35b"}.fa-tex:before{content:"\f35c"}.fa-textpattern:before{content:"\f35d"}.fa-unity:before{content:"\f35e"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}/*# sourceMappingURL=fork-awesome.min.css.map */ \ No newline at end of file diff --git a/assets/fonts/forkawesome-webfont.woff2 b/assets/fonts/forkawesome-webfont.woff2 new file mode 100755 index 0000000..c96e5bf Binary files /dev/null and b/assets/fonts/forkawesome-webfont.woff2 differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000..5dbbdbe Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/locales/en.yml b/locales/en.yml new file mode 100644 index 0000000..5e258e0 --- /dev/null +++ b/locales/en.yml @@ -0,0 +1,92 @@ +_version: 1 +name: "BookForge" + +common: + download: Download + create: Create + edit: Edit + delete: Delete + next: Next + previous: Previous + actions: Actions + search: Search + reset: Reset + show: Show + no_result: No results + close: Close + confirmation: Confirmation + are_you_sure: Are you sure? + +nav: + toggle: Toggle navigation + books: Books + users: Users + +theme: + light: Light + dark: Dark + +user: + attributes: + name: Name + owner_books: Owned books + borrowed_books: Borrowed books + + index: + title_tag: Users list | BookForge + title: All Users + button: Add a user + + edit: + title_tag: Edit user | BookForge + title: Edit + button: Edit user + + new: + title_tag: New user | BookForge + title: New user + button: Create user + +book: + attributes: + title: Title + authors: Author(s) + description: Description + owner: Owner + current_holder: Current holder + comment: Comment + + index: + title_tag: Books list | BookForge + title: All Books + + new: + title_tag: New book | BookForge + title: New Book + button: Create book + button_short: Add book + + edit: + title_tag: Edit book | BookForge + title: Edit book + button: Edit book + + show: + title_tag: Details | BookForge + book_details: Book details + user_details: User details + more_informations: More information + +footer: + message: Made with love & Fuck fascists! + +error: + error_404: + title_tag: Error 404 | BookForge + title: Oops! This page does not exist + subtitle: 404 NOT FOUND + button: Back to home + + generic: + title_tag: Error | BookForge + title: Oops! An error occurred diff --git a/locales/fr.yml b/locales/fr.yml new file mode 100644 index 0000000..22b0675 --- /dev/null +++ b/locales/fr.yml @@ -0,0 +1,77 @@ +_version: 1 +name: "BookForge" +common: + download: Télécharger + create: Créer + edit: Modifier + delete: Supprimer + next: Suivant + previous: Précédent + actions: Actions + search: Filtrer + reset: Réini. + show: Voir + no_result: Aucun résultat + close: Fermer + confirmation: Confirmation + are_you_sure: Êtes-vous sûr ? +nav: + toggle: Basculer la navigation + books: Livres + users: Utilisateurs +theme: + light: Light + dark: Dark +user: + attributes: + name: Nom + owner_books: Livres possédés + borrowed_books: Livres empruntés + index: + title_tag: Liste des utilisateur.ice.s | BookForge + title: Tous les utilisateur.ice.s + button: Ajouter un.e utilisateur.ice + edit: + title_tag: Modifier l'utilisateur.ice | BookForge + title: Modifier + button: Modifier l'utilisateur.ice + new: + title_tag: Nouvel utilisateur.ice | BookForge + title: Nouvel utilisateur.ice + button: Créer l'utilisateur.ice +book: + attributes: + title: Titre + authors: Auteur.ice.(s) + description: Description + owner: Propriétaire + current_holder: Détenteur.ice actuel.le + comment: Commentaire + index: + title_tag: Liste des livres | BookForge + title: Tous les livres + new: + title_tag: Nouveau livre | BookForge + title: Nouveau livre + button: Créer le livre + button_short: Ajouter un livre + edit: + title_tag: Modifier le livre | BookForge + title: Modifier le livre + button: Modifier le livre + show: + title_tag: Details | BookForge + book_details: Détails du livre + user_details: Détails de l'utilisateur.ice + more_informations: Plus d'informations +footer: + message: Fait avec amour & Nique les fachos ! +error: + error_404: + title_tag: Erreur 404 | BookForge + title: Oups ! Cette page n'existe pas + subtitle: 404 NOT FOUND + button: Retour à l'accueil + generic: + title_tag: Erreur | BookForge + title: Oups ! Une erreur s'est produite diff --git a/src/lib.rs b/src/lib.rs index d9a0fa0..6c3de23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,13 @@ +use askama::Template; +use askama_web::WebTemplate; use axum::{ Router, + extract::State, routing::{get, post}, }; use static_serve::embed_assets; +use crate::routes::router::Router as InternalRouter; use crate::state::AppState; mod migrations; @@ -11,20 +15,45 @@ mod models; mod routes; pub mod state; +#[macro_use] +extern crate rust_i18n; + +i18n!("locales", fallback = "en", minify_key = true); + pub fn build_app(state: AppState) -> Router { + rust_i18n::set_locale(&state.config.locale); embed_assets!("assets", compress = true); Router::new() .route("/", get(routes::book::index)) + .route("/books/new", get(routes::book::new)) .route("/books", post(routes::book::create)) .route("/books/{id}", get(routes::book::show)) .route("/books/{id}", post(routes::book::update)) .route("/books/{id}/delete", post(routes::book::delete)) .route("/books/{id}/edit", get(routes::book::edit)) - .route("/books/new", get(routes::book::new)) + .route("/books/download_csv", get(routes::book::download_csv)) .route("/users", get(routes::user::index)) + .route("/users/new", get(routes::user::new)) + .route("/users/{id}/edit", get(routes::user::edit)) + .route("/users/{id}", post(routes::user::update)) .route("/users", post(routes::user::create)) - .route("/users/{id}", post(routes::user::delete)) + .route("/users/{id}/delete", post(routes::user::delete)) .nest("/assets", static_router()) + .fallback(error_handler) .with_state(state) } + +#[derive(Template, WebTemplate)] +#[template(path = "404.html")] +struct NotFoundTemplate { + pub router: InternalRouter, +} + +pub async fn error_handler(State(state): State) -> impl axum::response::IntoResponse { + NotFoundTemplate { + router: InternalRouter { + base_path: state.config.base_path, + }, + } +} diff --git a/src/models/book.rs b/src/models/book.rs index ea84252..b61753a 100644 --- a/src/models/book.rs +++ b/src/models/book.rs @@ -70,7 +70,7 @@ impl BookOperator { /// Lists all books matching the optional query filters. /// /// Results are ordered by ID in descending order (newest first). - pub async fn list(&self) -> Result, BookError> { + pub async fn all(&self) -> Result, BookError> { Entity::find() .order_by_desc(Column::Id) .all(&self.state.db) @@ -78,7 +78,18 @@ impl BookOperator { .context(DBSnafu) } - pub async fn list_paginate( + pub async fn all_filtered(&self, query: Option) -> Result, BookError> { + let conditions = Self::filter_conditions(query); + + Entity::find() + .filter(conditions) + .order_by_desc(Column::Id) + .all(&self.state.db) + .await + .context(DBSnafu) + } + + pub async fn all_paginate( &self, page: u64, query: Option, @@ -86,29 +97,12 @@ impl BookOperator { let page = if page > 0 { page } else { 1 }; // keep 1-indexed let page_0indexed = page - 1; // convert for SeaORM (0-based index) - let mut conditions = Condition::all(); - if let Some(book_query) = query { - if let Some(title) = book_query.title { - conditions = conditions.add(Column::Title.contains(&title)); - } - - if let Some(authors) = book_query.authors { - conditions = conditions.add(Column::Authors.contains(&authors)); - } - - if let Some(owner_id) = book_query.owner_id { - conditions = conditions.add(Column::OwnerId.eq(owner_id)); - } - - if let Some(current_holder_id) = book_query.current_holder_id { - conditions = conditions.add(Column::CurrentHolderId.eq(current_holder_id)); - } - } + let conditions = Self::filter_conditions(query); let book_pages = Entity::find() .filter(conditions) .order_by_desc(Column::Id) - .paginate(&self.state.db, 1); + .paginate(&self.state.db, 100); let books = book_pages .fetch_page(page_0indexed) @@ -140,6 +134,27 @@ impl BookOperator { } } + /// Finds vec of book by its Owner + pub async fn find_all_by_owner(&self, owner_id: i32) -> Result, BookError> { + Entity::find() + .filter(Column::OwnerId.eq(owner_id)) + .all(&self.state.db) + .await + .context(DBSnafu) + } + + /// Finds vec of book by its Owner + pub async fn find_all_by_current_holder( + &self, + current_holder_id: i32, + ) -> Result, BookError> { + Entity::find() + .filter(Column::CurrentHolderId.eq(current_holder_id)) + .all(&self.state.db) + .await + .context(DBSnafu) + } + /// Creates a new book from the given form data. pub async fn create(&self, form: BookForm) -> Result { let book = ActiveModel { @@ -188,4 +203,28 @@ impl BookOperator { book.delete(&self.state.db).await.context(DBSnafu) } + + // private + + fn filter_conditions(query: Option) -> Condition { + let mut conditions = Condition::all(); + if let Some(book_query) = query { + if let Some(title) = book_query.title { + conditions = conditions.add(Column::Title.contains(&title)); + } + + if let Some(authors) = book_query.authors { + conditions = conditions.add(Column::Authors.contains(&authors)); + } + + if let Some(owner_id) = book_query.owner_id { + conditions = conditions.add(Column::OwnerId.eq(owner_id)); + } + + if let Some(current_holder_id) = book_query.current_holder_id { + conditions = conditions.add(Column::CurrentHolderId.eq(current_holder_id)); + } + } + return conditions; + } } diff --git a/src/models/user.rs b/src/models/user.rs index 0311fa4..0ce96eb 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,6 +1,11 @@ +use crate::models::book; +use crate::routes::book::BookForm; +use crate::routes::user::IndexQuery; use crate::routes::user::UserForm; use crate::state::AppState; +use crate::state::error::UserSnafu; use sea_orm::ActiveValue::Set; +use sea_orm::Condition; use sea_orm::DeleteResult; use sea_orm::entity::prelude::*; use snafu::ResultExt; @@ -36,6 +41,8 @@ pub enum UserError { DB { source: sea_orm::DbErr }, #[snafu(display("User with id {id} not found"))] NotFound { id: i32 }, + #[snafu(display("Book error"))] + Book { source: super::book::BookError }, } #[derive(Debug)] @@ -48,10 +55,23 @@ impl UserOperator { Self { state } } - pub async fn list(&self) -> Result, UserError> { + pub async fn all(&self) -> Result, UserError> { Entity::find().all(&self.state.db).await.context(DBSnafu) } + pub async fn all_filtered(&self, query: IndexQuery) -> Result, UserError> { + let mut conditions = Condition::all(); + if let Some(name) = query.name { + conditions = conditions.add(Column::Name.contains(name)) + } + + Entity::find() + .filter(conditions) + .all(&self.state.db) + .await + .context(DBSnafu) + } + pub async fn find_by_id(&self, id: i32) -> Result { let user: Option = Entity::find_by_id(id) .one(&self.state.db) @@ -74,7 +94,60 @@ impl UserOperator { user.insert(&self.state.db).await.context(DBSnafu) } + pub async fn update(&self, id: i32, form: UserForm) -> Result { + let user_by_id = Self::find_by_id(&self, id).await.context(UserSnafu); + + if let Ok(user) = user_by_id { + let mut user: ActiveModel = user.into(); + + user.name = Set(form.name); + + user.update(&self.state.db).await.context(DBSnafu) + } else { + Err(UserError::NotFound { id }) + } + } + + /// Delete user by ID. + /// Before deleting the user, you must search for all the books they own in order to delete them beforehand, + /// then search for all the books they have borrowed in order to update the current holder to None. pub async fn delete(&self, user_id: i32) -> Result { + // get all + let owner_books = book::BookOperator::new(self.state.clone()) + .find_all_by_owner(user_id) + .await + .context(BookSnafu)?; + + // Delete all book with owner_id = current_user + for owner_book in owner_books { + book::BookOperator::new(self.state.clone()) + .delete(owner_book.id) + .await + .context(BookSnafu)?; + } + + let current_holder_books = book::BookOperator::new(self.state.clone()) + .find_all_by_current_holder(user_id) + .await + .context(BookSnafu)?; + + // Update all book with current Holder = current user + for current_holder_book in current_holder_books { + let form = BookForm { + title: current_holder_book.title, + authors: current_holder_book.authors, + owner_id: current_holder_book.owner_id, + description: current_holder_book.description, + comment: current_holder_book.comment, + current_holder_id: None, + }; + + book::BookOperator::new(self.state.clone()) + .update(current_holder_book.id, form) + .await + .context(BookSnafu)?; + } + let user: Option = Entity::find_by_id(user_id) .one(&self.state.db) .await diff --git a/src/routes/book.rs b/src/routes/book.rs index 26545fb..8d4fa03 100644 --- a/src/routes/book.rs +++ b/src/routes/book.rs @@ -4,15 +4,17 @@ use askama::Template; use askama_web::WebTemplate; use axum::{ Form, + body::Body, extract::{Path, Query, State}, - response::{IntoResponse, Redirect}, + response::{IntoResponse, Redirect, Response}, }; +use csv::Writer; use serde::Deserialize; use serde_with::{NoneAsEmptyString, serde_as}; use snafu::prelude::*; -use crate::models::book::Model as BookModel; -use crate::models::user::Model as UserModel; +use crate::{models::book::Model as BookModel, routes::router::Router, state::error::CSVSnafu}; +use crate::{models::user::Model as UserModel, state::error::IOSnafu}; use crate::{ models::{book::BookOperator, user::UserOperator}, @@ -53,6 +55,7 @@ struct BookIndexTemplate { current_page: u64, total_page: u64, base_query: String, + router: Router, } pub async fn index( @@ -66,13 +69,13 @@ pub async fn index( // Get all Users let users = UserOperator::new(state.clone()) - .list() + .all() .await .context(UserSnafu)?; // Get all Book filtered with query - let books_paginate = BookOperator::new(state) - .list_paginate(page, Some(query.clone())) + let books_paginate = BookOperator::new(state.clone()) + .all_paginate(page, Some(query.clone())) .await .context(BookSnafu)?; @@ -125,6 +128,9 @@ pub async fn index( current_page: books_paginate.current_page, total_page: books_paginate.total_page, base_query, + router: Router { + base_path: state.config.base_path, + }, }) } @@ -134,6 +140,7 @@ struct ShowBookTemplate { book: BookModel, owner: UserModel, current_holder: Option, + router: Router, } pub async fn show( @@ -166,6 +173,9 @@ pub async fn show( book, owner, current_holder, + router: Router { + base_path: state.config.base_path, + }, }) } @@ -198,14 +208,23 @@ pub async fn create( #[template(path = "books/new.html")] struct NewBookTemplate { users: Vec, + router: Router, } pub async fn new( State(state): State, ) -> Result { - let users = UserOperator::new(state).list().await.context(UserSnafu)?; + let users = UserOperator::new(state.clone()) + .all() + .await + .context(UserSnafu)?; - Ok(NewBookTemplate { users }) + Ok(NewBookTemplate { + users, + router: Router { + base_path: state.config.base_path, + }, + }) } #[derive(Template, WebTemplate)] @@ -213,6 +232,7 @@ pub async fn new( struct EditBookTemplate { users: Vec, book: BookModel, + router: Router, } pub async fn edit( @@ -220,15 +240,21 @@ pub async fn edit( Path(id): Path, ) -> Result { let users = UserOperator::new(state.clone()) - .list() + .all() .await .context(UserSnafu)?; - let book = BookOperator::new(state) + let book = BookOperator::new(state.clone()) .find_by_id(id) .await .context(BookSnafu)?; - Ok(EditBookTemplate { users, book }) + Ok(EditBookTemplate { + users, + book, + router: Router { + base_path: state.config.base_path, + }, + }) } pub async fn update( @@ -254,3 +280,75 @@ pub async fn delete( Ok(Redirect::to("/").into_response()) } + +/// Download CSV filter (no paginate) of all books +pub async fn download_csv( + State(state): State, + Query(query): Query, +) -> Result { + let books = BookOperator::new(state.clone()) + .all_filtered(Some(query)) + .await + .context(BookSnafu)?; + + let users = UserOperator::new(state).all().await.context(UserSnafu)?; + + let users_by_id: HashMap = users.into_iter().map(|u| (u.id, u)).collect(); + + let mut wtr = Writer::from_writer(vec![]); + wtr.write_record(&[ + "ID", + "Title", + "Author(s)", + "Description", + "Owner", + "Current Holder", + "Comment", + ]) + .context(CSVSnafu)?; + + for book in books { + let owner_format = match users_by_id.get(&book.owner_id).cloned().ok_or(UserSnafu) { + Ok(owner) => format!("{} (id: {})", owner.name.to_string(), owner.id), + Err(_) => "-".to_string(), + }; + + let current_holder = match users_by_id + // if current_holder_id is None, take 0. + // So get returns errors because user with id 0 can't exist + .get(&book.current_holder_id.unwrap_or(0)) + .cloned() + .ok_or(UserSnafu) + { + Ok(current_holder) => format!( + "{} (id: {})", + current_holder.name.to_string(), + current_holder.id + ), + Err(_) => "-".to_string(), + }; + + wtr.write_record(&[ + book.id.to_string(), + book.title, + book.authors, + book.description.unwrap_or_default(), + owner_format, + current_holder, + book.comment.unwrap_or_default(), + ]) + .context(CSVSnafu)?; + } + + wtr.flush().context(IOSnafu)?; + + let csv_bytes = wtr.into_inner(); + + match csv_bytes { + Ok(csv_bytes) => Ok(Response::builder() + .header("Content-Type", "text/csv") + .body(Body::from(csv_bytes)) + .unwrap()), + Err(_) => Ok(Redirect::to("/").into_response()), + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 011ff79..2bbba56 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,2 +1,3 @@ pub mod book; +pub mod router; pub mod user; diff --git a/src/routes/router.rs b/src/routes/router.rs new file mode 100644 index 0000000..52bcd0b --- /dev/null +++ b/src/routes/router.rs @@ -0,0 +1,54 @@ +#[derive(Clone)] +pub struct Router { + pub base_path: String, +} + +impl Router { + pub fn assets(&self, path: &str) -> String { + if self.base_path.is_empty() || self.base_path == "/" { + format!("/{}", path) + } else { + format!("{}/{}", self.base_path.trim_end_matches('/'), path) + } + } + + pub fn root_path(&self) -> String { + format!("{}/", &self.base_path) + } + + // BOOKS ROUTES + + pub fn new_book_path(&self) -> String { + format!("{}/books/new", &self.base_path) + } + + pub fn create_book_path(&self) -> String { + format!("{}/books", &self.base_path) + } + + pub fn update_book_path(&self, id: &i32) -> String { + format!("{}/books/{}", &self.base_path, id) + } + + pub fn download_csv_book_path(&self) -> String { + format!("{}/books/download_csv", &self.base_path) + } + + // USERS + + pub fn index_user_path(&self) -> String { + format!("{}/users", &self.base_path) + } + + pub fn new_user_path(&self) -> String { + format!("{}/users/new", &self.base_path) + } + + pub fn create_user_path(&self) -> String { + format!("{}/users", &self.base_path) + } + + pub fn update_user_path(&self, id: &i32) -> String { + format!("{}/users/{}", &self.base_path, id) + } +} diff --git a/src/routes/user.rs b/src/routes/user.rs index 8a49cb4..04eed56 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -4,10 +4,11 @@ use askama::Template; use askama_web::WebTemplate; use axum::{ Form, - extract::{Path, State}, + extract::{Path, Query, State}, response::Redirect, }; use serde::Deserialize; +use serde_with::{NoneAsEmptyString, serde_as}; use snafu::prelude::*; use crate::{ @@ -15,6 +16,7 @@ use crate::{ book::BookOperator, user::{self, UserOperator}, }, + routes::router::Router, state::{ AppState, error::{AppStateError, BookSnafu, UserSnafu}, @@ -24,23 +26,43 @@ use crate::{ #[derive(Template, WebTemplate)] #[template(path = "users/index.html")] struct UsersIndexTemplate { - user_with_books_number: Vec<(user::Model, usize, usize)>, + users_with_books_number: Vec, + query: IndexQuery, + router: Router, +} + +pub struct UserWithBookNumber { + /// the user model + pub user: user::Model, + /// the number of books owned by this user + pub owner_book_number: usize, + /// the number of books borrowed by this user + pub borrowed_book_number: usize, +} + +#[serde_as] +#[derive(Deserialize, Clone)] +pub struct IndexQuery { + #[serde(default)] + #[serde_as(as = "NoneAsEmptyString")] + pub name: Option, } pub async fn index( State(state): State, + Query(query): Query, ) -> Result { let users = UserOperator::new(state.clone()) - .list() + .all_filtered(query.clone()) .await .context(UserSnafu)?; let books = BookOperator::new(state.clone()) - .list() + .all() .await .context(BookSnafu)?; - let mut result: Vec<(user::Model, usize, usize)> = vec![]; + let mut result: Vec = Vec::with_capacity(users.len()); let mut owner_books: HashMap = HashMap::new(); let mut borrowed_books: HashMap = HashMap::new(); @@ -56,11 +78,19 @@ pub async fn index( let owner_books_size = owner_books.get(&user.id).unwrap_or(&0); let borrowed_books_size = borrowed_books.get(&user.id).unwrap_or(&0); - result.push((user, *owner_books_size, *borrowed_books_size)); + result.push(UserWithBookNumber { + user, + owner_book_number: *owner_books_size, + borrowed_book_number: *borrowed_books_size, + }); } Ok(UsersIndexTemplate { - user_with_books_number: result, + users_with_books_number: result, + query, + router: Router { + base_path: state.config.base_path, + }, }) } @@ -73,7 +103,7 @@ pub async fn create( State(state): State, Form(form): Form, ) -> Result { - let _user = UserOperator::new(state) + let _ = UserOperator::new(state) .create(form) .await .context(UserSnafu)?; @@ -81,6 +111,19 @@ pub async fn create( Ok(Redirect::to("/users")) } +pub async fn update( + State(state): State, + Path(id): Path, + Form(form): Form, +) -> Result { + let _ = UserOperator::new(state) + .update(id, form) + .await + .context(UserSnafu)?; + + Ok(Redirect::to("/users")) +} + pub async fn delete( State(state): State, Path(id): Path, @@ -92,3 +135,41 @@ pub async fn delete( Ok(Redirect::to("/users")) } + +#[derive(Template, WebTemplate)] +#[template(path = "users/edit.html")] +struct EditTemplate { + user: user::Model, + router: Router, +} + +pub async fn edit( + State(state): State, + Path(id): Path, +) -> Result { + let user = UserOperator::new(state.clone()) + .find_by_id(id) + .await + .context(UserSnafu)?; + + Ok(EditTemplate { + user, + router: Router { + base_path: state.config.base_path, + }, + }) +} + +#[derive(Template, WebTemplate)] +#[template(path = "users/new.html")] +struct NewTemplate { + router: Router, +} + +pub async fn new(State(state): State) -> impl axum::response::IntoResponse { + NewTemplate { + router: Router { + base_path: state.config.base_path, + }, + } +} diff --git a/src/state/config.rs b/src/state/config.rs index 4f81560..f137f8a 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -32,6 +32,8 @@ pub enum ConfigError { pub struct AppConfig { #[serde(default = "AppConfig::default_sqlite_path")] pub database_path: Utf8PathBuf, + pub locale: String, + pub base_path: String, pub listener: Listener, } @@ -39,6 +41,8 @@ impl Default for AppConfig { fn default() -> Self { AppConfig { database_path: Self::default_sqlite_path(), + base_path: Self::default_base_path(), + locale: Self::default_locale(), listener: Listener::default(), } } @@ -92,6 +96,14 @@ impl AppConfig { Self::config_path().join("BookForge.toml") } + fn default_locale() -> String { + "en".to_string() + } + + fn default_base_path() -> String { + "".to_string() + } + pub fn default_sqlite_path() -> Utf8PathBuf { Self::config_path().join("db.sqlite") } diff --git a/src/state/error.rs b/src/state/error.rs index 36dedc1..6a031b6 100644 --- a/src/state/error.rs +++ b/src/state/error.rs @@ -1,17 +1,15 @@ use askama::Template; use askama_web::WebTemplate; use axum::response::{IntoResponse, Response}; +use log::error; use snafu::prelude::*; use crate::{ models::{book::BookError, user::UserError}, + routes::router::Router, state::config::ConfigError, }; -#[derive(Template, WebTemplate)] -#[template(path = "error.html")] -struct ErrorTemplate {} - #[derive(Snafu, Debug)] #[snafu(visibility(pub))] pub enum AppStateError { @@ -36,10 +34,44 @@ pub enum AppStateError { Book { source: BookError, }, + #[snafu(display("CSV Error"))] + CSV { + source: csv::Error, + }, + #[snafu(display("IO Error"))] + IO { + source: std::io::Error, + }, +} + +#[derive(Template, WebTemplate)] +#[template(path = "error.html")] +struct ErrorTemplate { + state: AppStateErrorContext, + router: Router, +} + +struct AppStateErrorContext { + pub errors: Vec, +} + +impl From for AppStateErrorContext { + fn from(e: AppStateError) -> Self { + error!("{:?}", e); + + Self { errors: vec![e] } + } } impl IntoResponse for AppStateError { fn into_response(self) -> Response { - ErrorTemplate {}.into_response() + let error_context = AppStateErrorContext::from(self); + ErrorTemplate { + state: error_context, + router: Router { + base_path: "".to_string(), + }, + } + .into_response() } } diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..6f35ef6 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %} + {{ t!("error.error_404.title_tag") }} +{% endblock %} + +{% block main %} +
+

{{ t!("error.error_404.title") }}

+

{{ t!("error.error_404.subtitle") }}

+ {{ t!("error.error_404.button") }} +
+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 5e2038d..bc0ed7e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,13 +2,14 @@ - {% block title %}Book Forge{% endblock %} + {% block title %}{{ t!("name") }}{% endblock %} - - + + + - - + + {% block extra_head %}{% endblock extra_head %} @@ -25,8 +26,7 @@ {% endblock %} - - + + - diff --git a/templates/books/edit.html b/templates/books/edit.html index 6964b99..f80be6a 100644 --- a/templates/books/edit.html +++ b/templates/books/edit.html @@ -2,23 +2,27 @@ {% import "components/typography.html" as typography %} {% import "components/cards.html" as cards %} +{% block title %} + {{ t!("book.edit.title_tag") }} +{% endblock %} + {% block main %} - {{ typography::heading("Editer") }} + {{ typography::heading(t!("book.edit.title")) }} {% call cards::card() %} -
+
- +
- +
- + {% match book.current_holder_id %} @@ -52,7 +56,7 @@
- + {% match book.description %} {% when Some with (description) %} @@ -62,7 +66,7 @@
- + {% match book.comment %} {% when Some with (comment) %} @@ -72,7 +76,7 @@
- +
{% endcall %} diff --git a/templates/books/new.html b/templates/books/new.html index b37a020..30deb91 100644 --- a/templates/books/new.html +++ b/templates/books/new.html @@ -1,53 +1,34 @@ {% extends "base.html" %} {% import "components/typography.html" as typography %} {% import "components/cards.html" as cards %} +{% import "components/inputs.html" as form_helpers %} + +{% block title %} + {{ t!("book.new.title_tag") }} +{% endblock %} {% block main %} - {{ typography::heading("New book") }} + {{ typography::heading(t!("book.new.title")) }} {% call cards::card() %} -
-
- - -
+ + {{ form_helpers::input("title", t!("book.attributes.title"), is_required = true, placeholder = "Ex: La Petite Dernière") }} + {{ form_helpers::input("authors", t!("book.attributes.authors"), is_required = true, placeholder = "Ex: Fatima Daas") }} -
- - -
+ {% call(option) form_helpers::select("owner_id", t!("book.attributes.owner"), users, is_required = true) %} + + {% endcall %} -
- - -
+ {% call(option) form_helpers::select("current_holder_id", t!("book.attributes.current_holder"), users, is_required = false) %} + + {% endcall %} -
- - -
+ {{ form_helpers::textarea("description", t!("book.attributes.description"), rows = 5, is_required = false, placeholder = "Ex: Je m’appelle Fatima Daas. Je suis la mazoziya, la petite dernière. Celle à laquelle on ne s’est pas préparé. Française d’origine algérienne.") }} -
- - -
- -
- - -
+ {{ form_helpers::textarea("comment", t!("book.attributes.comment"), rows = 3, is_required = false, placeholder = "Ex: I recommend it, it's great!") }}
- +
{% endcall %} diff --git a/templates/books/show.html b/templates/books/show.html index aec5bda..61042f2 100644 --- a/templates/books/show.html +++ b/templates/books/show.html @@ -4,39 +4,43 @@ {% import "components/fields.html" as fields %} {% import "components/cards.html" as cards %} +{% block title %} + {{ t!("book.show.title_tag") }} +{% endblock %} + {% block main %} {{ typography::book_heading(book.title, book, show = false) }} {% call cards::card() %} -
-
Book details
- {{ fields::field("Name", book.title) }} - {{ fields::field("Authors", book.authors) }} +
+
{{ t!("book.show.book_details") }}
+ {{ fields::field(t!("book.attributes.title"), book.title) }} + {{ fields::field(t!("book.attributes.authors"), book.authors) }} {% match book.description %} {% when Some with (description) %} - {{ fields::field("Description", description) }} + {{ fields::field(t!("book.attributes.description"), description) }} {% when None %} - {{ fields::field("Description", "-") }} + {{ fields::field(t!("book.attributes.description"), "-") }} {% endmatch %} -
User Details
- {{ fields::field("Owner", owner.name) }} +
{{ t!("book.show.user_details") }}
+ {{ fields::field(t!("book.attributes.owner"), owner.name) }} {% match current_holder %} {% when Some with (current_holder) %} - {{ fields::field("Current Holder", current_holder.name) }} + {{ fields::field(t!("book.attributes.current_holder"), current_holder.name) }} {% when None %} - {{ fields::field("Current Holder", "-") }} + {{ fields::field(t!("book.attributes.current_holder"), "-") }} {% endmatch %} -
More Informations
+
{{ t!("book.show.more_informations") }}
{% match book.comment %} {% when Some with (comment) %} - {{ fields::field("Comment", comment) }} + {{ fields::field(t!("book.attributes.comment"), comment) }} {% when None %} - {{ fields::field("Comment", "-") }} + {{ fields::field(t!("book.attributes.comment"), "-") }} {% endmatch %}
{% endcall %} diff --git a/templates/components/dropdown.html b/templates/components/dropdown.html index a792d72..7b0e55d 100644 --- a/templates/components/dropdown.html +++ b/templates/components/dropdown.html @@ -11,21 +11,40 @@
{% endmacro %} -{% macro book_dropdown_button(book, show = true) %} +{% macro crud_dropdown_button(book, label, sub_path, show = true) %} + + + {% endmacro %} diff --git a/templates/components/inputs.html b/templates/components/inputs.html new file mode 100644 index 0000000..6b6082d --- /dev/null +++ b/templates/components/inputs.html @@ -0,0 +1,43 @@ +{% macro input(name, label, value = "", type = "text", is_required = false, placeholder = "", margin_bottom = true) %} +
+ + +
+{% endmacro %} + +{% macro select(name, label, options, selected_value = "", is_required = false, margin_bottom = true) -%} +
+ + +
+{%- endmacro %} + +{% macro textarea(name, label, value = "", rows = 2, is_required = false, placeholder = "", margin_bottom = true) %} +
+ + +
+{% endmacro %} + diff --git a/templates/components/typography.html b/templates/components/typography.html index 1068b83..3598c9a 100644 --- a/templates/components/typography.html +++ b/templates/components/typography.html @@ -21,7 +21,7 @@
- {{ dropdown::book_dropdown_button(book, show) }} + {{ dropdown::crud_dropdown_button(book, t!("common.actions"), "books", show) }}
{% endmacro %} diff --git a/templates/error.html b/templates/error.html index 59fd4d6..09e125b 100644 --- a/templates/error.html +++ b/templates/error.html @@ -1 +1,17 @@ -Error +{% extends "base.html" %} + +{% block title %} + {{ t!("error.generic.title_tag") }} +{% endblock %} + +{% block main %} +
+

{{ t!("error.generic.title") }}

+
+ {% for error in state.errors %} +

{{ error }}

+ {% endfor %} +
+ {{ t!("error.error_404.button") }} +
+{% endblock %} diff --git a/templates/index.html b/templates/index.html index a1882c9..2701d7b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,14 +3,22 @@ {% import "components/dropdown.html" as dropdown %} {% import "components/cards.html" as cards %} +{% block title %} + {{ t!("book.index.title_tag") }} +{% endblock %} + {% block main %} - {{ typography::heading("All books") }} + {% call typography::heading(t!("book.index.title")) %} + + {{ t!("common.download") }} (csv) + + {% endcall %} {% call cards::card() %}
- + {% match query.title %} {% when Some with (value) %} @@ -20,7 +28,7 @@
- + {% match query.authors %} {% when Some with (value) %} @@ -31,9 +39,9 @@
- + - {% match query.owner_id %} {% when Some with (owner_id) %} @@ -53,9 +61,9 @@
- + - {% match query.current_holder_id %} {% when Some with (current_holder_id) %} @@ -75,67 +83,84 @@
- + +
+ +
{% endcall %} {% call cards::card() %} - - - - - - - - - - - - - {% for book_user in books_with_user %} - - - - - - - - - {% endfor %} - -
#NameAuthor(s)OwnerCurrent HolderActions
{{ book_user.book.id }}{{ book_user.book.title }}{{ book_user.book.authors }}{{ book_user.owner.name }} - {% match book_user.current_holder %} - {% when Some with (current_holder) %} - {{ current_holder.name }} - {% when None %} - - - {% endmatch %} - - {{ dropdown::book_dropdown_button(book_user.book) }} -
- - {% if total_page > 1 %} - + + {{ t!("book.new.button_short") }} + + + {% else %} + + + + + + + + + + + + + {% for book_user in books_with_user %} + + + + + + + + + {% endfor %} + +
#{{ t!("book.attributes.title") }}{{ t!("book.attributes.authors") }}{{ t!("book.attributes.owner") }}{{ t!("book.attributes.current_holder") }}{{ t!("common.actions") }}
{{ book_user.book.id }}{{ book_user.book.title }}{{ book_user.book.authors }}{{ book_user.owner.name }} + {% match book_user.current_holder %} + {% when Some with (current_holder) %} + {{ current_holder.name }} + {% when None %} + - + {% endmatch %} + + {{ dropdown::crud_dropdown_button(book_user.book, t!("common.actions"), "books") }} +
+ + {% if total_page > 1 %} +
+ +
+ {% endif %} + {% endif %} {% endcall %} {% endblock %} diff --git a/templates/layout/footer.html b/templates/layout/footer.html index 3ddc19e..3f29c8b 100644 --- a/templates/layout/footer.html +++ b/templates/layout/footer.html @@ -1,5 +1,5 @@
-

Made with Love & Fuck a Fascist !

+

{{ t!("footer.message") }}

-
\ No newline at end of file + diff --git a/templates/layout/header.html b/templates/layout/header.html index 3f64943..114aa1f 100644 --- a/templates/layout/header.html +++ b/templates/layout/header.html @@ -1,34 +1,33 @@ \ No newline at end of file + diff --git a/templates/users/edit.html b/templates/users/edit.html new file mode 100644 index 0000000..017ef04 --- /dev/null +++ b/templates/users/edit.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% import "components/typography.html" as typography %} +{% import "components/cards.html" as cards %} +{% import "components/inputs.html" as form_helpers %} + +{% block title %} + {{ t!("book.edit.title_tag") }} +{% endblock %} + +{% block main %} + {{ typography::heading(t!("user.edit.title")) }} + + {% call cards::card() %} +
+
+
+ {{ form_helpers::input("name", t!("user.attributes.name"), value = user.name, is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }} +
+ +
+ +
+
+
+ {% endcall %} +{% endblock %} diff --git a/templates/users/index.html b/templates/users/index.html index 6d424d2..835aa99 100644 --- a/templates/users/index.html +++ b/templates/users/index.html @@ -1,46 +1,76 @@ {% extends "base.html" %} {% import "components/typography.html" as typography %} +{% import "components/dropdown.html" as dropdown %} {% import "components/cards.html" as cards %} +{% import "components/inputs.html" as form_helpers %} + +{% block title %} + {{ t!("user.index.title_tag") }} +{% endblock %} {% block main %} - {{ typography::heading("All users") }} - - {% call cards::card() %} - - - - - - - - - - - - {% for (user, book_size, borrowed_book) in user_with_books_number %} - - - - - - - - {% endfor %} - -
#NameOwner bookBorrowed bookActions
{{ user.id }}{{ user.name }}{{ book_size }}{{ borrowed_book }} -
- -
-
+ {% call typography::heading(t!("user.index.title")) %} + {{ t!("user.index.button") }} {% endcall %} {% call cards::card() %} -
- - + +
+
+ + {% match query.name %} + {% when Some with (value) %} + + {% when None %} + + {% endmatch %} +
- +
+ +
+ + +
{% endcall %} + {% call cards::card() %} + {% if users_with_books_number.is_empty() %} +
+

{{ t!("common.no_result") }}

+ + + {{ t!("user.index.button") }} + +
+ {% else %} + + + + + + + + + + + + {% for user_information in users_with_books_number %} + + + + + + + + {% endfor %} + +
#{{ t!("user.attributes.name") }}{{ t!("user.attributes.owner_books") }}{{ t!("user.attributes.borrowed_books") }}{{ t!("common.actions") }}
{{ user_information.user.id }}{{ user_information.user.name }}{{ user_information.owner_book_number }}{{ user_information.borrowed_book_number }} + {{ dropdown::crud_dropdown_button(user_information.user, t!("common.actions"), "users", show = false) }} +
+ {% endif %} + {% endcall %} {% endblock %} diff --git a/templates/users/new.html b/templates/users/new.html new file mode 100644 index 0000000..6213b3a --- /dev/null +++ b/templates/users/new.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% import "components/typography.html" as typography %} +{% import "components/cards.html" as cards %} +{% import "components/inputs.html" as form_helpers %} + +{% block title %} + {{ t!("user.new.title_tag") }} +{% endblock %} + +{% block main %} + {{ typography::heading(t!("user.new.title")) }} + + {% call cards::card() %} +
+
+
+ {{ form_helpers::input("name", t!("user.attributes.name"), is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }} +
+
+ +
+
+
+ {% endcall %} +{% endblock %}