Compare commits

...

3 Commits

Author SHA1 Message Date
77cbb84885 Working sign in 2023-08-15 12:34:58 +02:00
2901da51f5 Working sign in 2023-08-15 12:33:53 +02:00
29e6e75e95 Fix navbar 2023-08-15 10:26:22 +02:00
34 changed files with 214 additions and 2543 deletions

View File

@ -1,7 +1,7 @@
(()=>{var c=Object.create;var i=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var h=Object.getPrototypeOf,p=Object.prototype.hasOwnProperty;var u=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,s)=>(typeof require<"u"?require:e)[s]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var m=(t,e,s,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of a(e))!p.call(t,o)&&o!==s&&i(t,o,{get:()=>e[o],enumerable:!(l=d(e,o))||l.enumerable});return t};var y=(t,e,s)=>(s=t!=null?c(h(t)):{},m(e||!t||!t.__esModule?i(s,"default",{value:t,enumerable:!0}):s,t));customElements.define("oswilno-price",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){let t=this.shadowRoot,e=parseInt(this.getAttribute("price"));isNaN(e)&&(e=0);let s=parseInt(this.getAttribute("multiplier")),l=e,o=0;isNaN(s)||(l=Math.floor(e/s),o=e%s);let r=this.getAttribute("currency")||"PLN";t.innerHTML=`<style>:host{display:block;}</style><div>${l}.${o>=10?o:o+"0"} ${r}</div>`}});var n=t=>{let e="";for(let l=0;l<document.styleSheets.length;l++){let o=document.styleSheets[l];for(let r=0;r<o.rules.length;r++)e+=o.rules[r].cssText}let s=new CSSStyleSheet;s.replaceSync(e),t.adoptedStyleSheets=[s]};customElements.define("oswilno-error",class extends HTMLElement{constructor(){super();let t=this.attachShadow({mode:"open"});t.innerHTML=`
(()=>{var a=Object.create;var n=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var u=Object.getPrototypeOf,m=Object.prototype.hasOwnProperty;var p=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,s)=>(typeof require<"u"?require:t)[s]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var f=(e,t,s,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of h(t))!m.call(e,o)&&o!==s&&n(e,o,{get:()=>t[o],enumerable:!(r=d(t,o))||r.enumerable});return e};var g=(e,t,s)=>(s=e!=null?a(u(e)):{},f(t||!e||!e.__esModule?n(s,"default",{value:e,enumerable:!0}):s,e));customElements.define("oswilno-price",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){let e=this.shadowRoot,t=parseInt(this.getAttribute("price"));isNaN(t)&&(t=0);let s=parseInt(this.getAttribute("multiplier")),r=t,o=0;isNaN(s)||(r=Math.floor(t/s),o=t%s);let l=this.getAttribute("currency")||"PLN";e.innerHTML=`<style>:host{display:block;}</style><div>${r}.${o>=10?o:o+"0"} ${l}</div>`}});var i=e=>{let t="";for(let r=0;r<document.styleSheets.length;r++){let o=document.styleSheets[r];for(let l=0;l<o.rules.length;l++)t+=o.rules[l].cssText}let s=new CSSStyleSheet;s.replaceSync(t),e.adoptedStyleSheets=[s]};customElements.define("oswilno-error",class extends HTMLElement{constructor(){super();let e=this.attachShadow({mode:"open"});e.innerHTML=`
<style>:host{display:block;}</style>
<div class="flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700">
<slot></slot>
<div>
`,n(t)}});import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");document.body.addEventListener("htmx:beforeOnLoad",function(t){let e=t.detail.xhr.status;(e===422||e===400)&&(t.detail.shouldSwap=!0,t.detail.isError=!1)});})();
`,i(e)}});import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");var c=document.body;c.addEventListener("htmx:beforeOnLoad",function(e){console.log(e);let t=e.detail,s=t.xhr,r=s.status,o=t.successful;if(console.log(o,s.getResponseHeader("Authorization")),r===200){let l=s.getResponseHeader("Authorization");l&&localStorage.setItem("jwt",l.replace(/^Bearer /i,""))}else r===401&&localStorage.removeItem("jwt");(r===422||r===400)&&(t.shouldSwap=!0,t.isError=!1)});c.addEventListener("htmx:configRequest",function(e){localStorage.getItem("jwt")&&(e.detail.headers.Authorization="Bearer "+(localStorage.getItem("jwt")||""))});})();
//# sourceMappingURL=build.js.map

View File

@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../crates/web-assets/assets/elements/oswilno-price.js", "../crates/web-assets/assets/css.js", "../crates/web-assets/assets/elements/oswilno-error.js", "../crates/web-assets/assets/app.js"],
"sourcesContent": ["customElements.define('oswilno-price', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.attachShadow({ mode: 'open' });\n\t}\n\tconnectedCallback() {\n\t\tlet shadow = this.shadowRoot;\n\t\tlet price = parseInt(this.getAttribute('price'));\n\t\tif (isNaN(price)) price = 0;\n\t\tconst multiplier = parseInt(this.getAttribute('multiplier'));\n\t\tlet major = price;\n\t\tlet minor = 0;\n\t\tif (!isNaN(multiplier)) {\n\t\t\tmajor = Math.floor(price / multiplier);\n\t\t\tminor = price % multiplier;\n\t\t}\n\t\tconst currency = this.getAttribute('currency') || 'PLN';\n\t\tshadow.innerHTML = `<style>:host{display:block;}</style><div>${major}.${minor >= 10 ? minor : minor + '0'} ${ currency }</div>`;\n\t}\n});\n", "export const copyCss = (shadow) => {\n\tlet css = '';\n\tfor (let i = 0; i < document.styleSheets.length; i++) {\n\t\tconst styleSheet = document.styleSheets[i];\n\t\tfor (let j = 0; j < styleSheet.rules.length; j++) {\n\t\t\tcss += styleSheet.rules[j].cssText;\t\n\t\t}\n\t}\n\tconst sheet = new CSSStyleSheet();\n\tsheet.replaceSync(css);\n\tshadow.adoptedStyleSheets = [sheet];\n};\n", "import { copyCss } from \"../css.js\";\n\ncustomElements.define('oswilno-error', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst shadow = this.attachShadow({ mode: 'open' });\n\t\tshadow.innerHTML = `\n\t\t\t<style>:host{display:block;}</style>\n\t\t\t<div class=\"flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700\">\n\t\t\t\t<slot></slot>\n\t\t\t<div>\n\t\t`;\n\t\tcopyCss(shadow);\n\t}\n});\n", "import './elements/oswilno-price.js';\nimport './elements/oswilno-error.js';\nimport(\"https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js\");\n\ndocument.body.addEventListener('htmx:beforeOnLoad', function (evt) {\n\tconst status = evt.detail.xhr.status;\n\tif (status === 422 || status === 400) {\n\t\tevt.detail.shouldSwap = true;\n\t\tevt.detail.isError = false;\n\t}\n});\n\n"],
"mappings": "0sBAAA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,CACnC,CACA,mBAAoB,CACnB,IAAIA,EAAS,KAAK,WACdC,EAAQ,SAAS,KAAK,aAAa,OAAO,CAAC,EAC3C,MAAMA,CAAK,IAAGA,EAAQ,GAC1B,IAAMC,EAAa,SAAS,KAAK,aAAa,YAAY,CAAC,EACvDC,EAAQF,EACRG,EAAQ,EACP,MAAMF,CAAU,IACpBC,EAAQ,KAAK,MAAMF,EAAQC,CAAU,EACrCE,EAAQH,EAAQC,GAEjB,IAAMG,EAAW,KAAK,aAAa,UAAU,GAAK,MAClDL,EAAO,UAAY,4CAA4CG,CAAK,IAAIC,GAAS,GAAKA,EAAQA,EAAQ,GAAG,IAAKC,CAAS,QACxH,CACD,CAAC,ECnBM,IAAMC,EAAWC,GAAW,CAClC,IAAIC,EAAM,GACV,QAASC,EAAI,EAAGA,EAAI,SAAS,YAAY,OAAQA,IAAK,CACrD,IAAMC,EAAa,SAAS,YAAYD,CAAC,EACzC,QAASE,EAAI,EAAGA,EAAID,EAAW,MAAM,OAAQC,IAC5CH,GAAOE,EAAW,MAAMC,CAAC,EAAE,OAE7B,CACA,IAAMC,EAAQ,IAAI,cAClBA,EAAM,YAAYJ,CAAG,EACrBD,EAAO,mBAAqB,CAACK,CAAK,CACnC,ECTA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,IAAMC,EAAS,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,EACjDA,EAAO,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAMnBC,EAAQD,CAAM,CACf,CACD,CAAC,ECZD,OAAO,mDAAmD,EAE1D,SAAS,KAAK,iBAAiB,oBAAqB,SAAUE,EAAK,CAClE,IAAMC,EAASD,EAAI,OAAO,IAAI,QAC1BC,IAAW,KAAOA,IAAW,OAChCD,EAAI,OAAO,WAAa,GACxBA,EAAI,OAAO,QAAU,GAEvB,CAAC",
"names": ["shadow", "price", "multiplier", "major", "minor", "currency", "copyCss", "shadow", "css", "i", "styleSheet", "j", "sheet", "shadow", "copyCss", "evt", "status"]
"sourcesContent": ["customElements.define('oswilno-price', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.attachShadow({ mode: 'open' });\n\t}\n\tconnectedCallback() {\n\t\tlet shadow = this.shadowRoot;\n\t\tlet price = parseInt(this.getAttribute('price'));\n\t\tif (isNaN(price)) price = 0;\n\t\tconst multiplier = parseInt(this.getAttribute('multiplier'));\n\t\tlet major = price;\n\t\tlet minor = 0;\n\t\tif (!isNaN(multiplier)) {\n\t\t\tmajor = Math.floor(price / multiplier);\n\t\t\tminor = price % multiplier;\n\t\t}\n\t\tconst currency = this.getAttribute('currency') || 'PLN';\n\t\tshadow.innerHTML = `<style>:host{display:block;}</style><div>${major}.${minor >= 10 ? minor : minor + '0'} ${ currency }</div>`;\n\t}\n});\n", "export const copyCss = (shadow) => {\n\tlet css = '';\n\tfor (let i = 0; i < document.styleSheets.length; i++) {\n\t\tconst styleSheet = document.styleSheets[i];\n\t\tfor (let j = 0; j < styleSheet.rules.length; j++) {\n\t\t\tcss += styleSheet.rules[j].cssText;\t\n\t\t}\n\t}\n\tconst sheet = new CSSStyleSheet();\n\tsheet.replaceSync(css);\n\tshadow.adoptedStyleSheets = [sheet];\n};\n", "import { copyCss } from \"../css.js\";\n\ncustomElements.define('oswilno-error', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst shadow = this.attachShadow({ mode: 'open' });\n\t\tshadow.innerHTML = `\n\t\t\t<style>:host{display:block;}</style>\n\t\t\t<div class=\"flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700\">\n\t\t\t\t<slot></slot>\n\t\t\t<div>\n\t\t`;\n\t\tcopyCss(shadow);\n\t}\n});\n", "import './elements/oswilno-price.js';\nimport './elements/oswilno-error.js';\nimport(\"https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js\");\n\nconst body = document.body;\nbody.addEventListener('htmx:beforeOnLoad', function (evt) {\n\tconsole.log(evt);\n\tconst detail = evt.detail;\n\tconst xhr = detail.xhr;\n\tconst status = xhr.status;\n\tconst successful = detail.successful;\n\n\tconsole.log(successful, xhr.getResponseHeader('Authorization'));\n\tif (status === 200) {\n\t\tconst bearer = xhr.getResponseHeader('Authorization');\n\t\tif (bearer) {\n\t\t\tlocalStorage.setItem('jwt', bearer.replace(/^Bearer /i, ''));\n\t\t}\n\t} else if (status === 401) {\n\t\tlocalStorage.removeItem('jwt');\n\t}\n\tif (status === 422 || status === 400) {\n\t\tdetail.shouldSwap = true;\n\t\tdetail.isError = false;\n\t}\n});\nbody.addEventListener('htmx:configRequest', function (evt) {\n\tif (localStorage.getItem('jwt')) {\n\tevt.detail.headers.Authorization = 'Bearer ' + (localStorage.getItem('jwt') || '');\n\t}\n});\n\n"],
"mappings": "0sBAAA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,CACnC,CACA,mBAAoB,CACnB,IAAIA,EAAS,KAAK,WACdC,EAAQ,SAAS,KAAK,aAAa,OAAO,CAAC,EAC3C,MAAMA,CAAK,IAAGA,EAAQ,GAC1B,IAAMC,EAAa,SAAS,KAAK,aAAa,YAAY,CAAC,EACvDC,EAAQF,EACRG,EAAQ,EACP,MAAMF,CAAU,IACpBC,EAAQ,KAAK,MAAMF,EAAQC,CAAU,EACrCE,EAAQH,EAAQC,GAEjB,IAAMG,EAAW,KAAK,aAAa,UAAU,GAAK,MAClDL,EAAO,UAAY,4CAA4CG,CAAK,IAAIC,GAAS,GAAKA,EAAQA,EAAQ,GAAG,IAAKC,CAAS,QACxH,CACD,CAAC,ECnBM,IAAMC,EAAWC,GAAW,CAClC,IAAIC,EAAM,GACV,QAASC,EAAI,EAAGA,EAAI,SAAS,YAAY,OAAQA,IAAK,CACrD,IAAMC,EAAa,SAAS,YAAYD,CAAC,EACzC,QAASE,EAAI,EAAGA,EAAID,EAAW,MAAM,OAAQC,IAC5CH,GAAOE,EAAW,MAAMC,CAAC,EAAE,OAE7B,CACA,IAAMC,EAAQ,IAAI,cAClBA,EAAM,YAAYJ,CAAG,EACrBD,EAAO,mBAAqB,CAACK,CAAK,CACnC,ECTA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,IAAMC,EAAS,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,EACjDA,EAAO,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAMnBC,EAAQD,CAAM,CACf,CACD,CAAC,ECZD,OAAO,mDAAmD,EAE1D,IAAME,EAAO,SAAS,KACtBA,EAAK,iBAAiB,oBAAqB,SAAUC,EAAK,CACzD,QAAQ,IAAIA,CAAG,EACf,IAAMC,EAASD,EAAI,OACbE,EAAMD,EAAO,IACbE,EAASD,EAAI,OACbE,EAAaH,EAAO,WAG1B,GADA,QAAQ,IAAIG,EAAYF,EAAI,kBAAkB,eAAe,CAAC,EAC1DC,IAAW,IAAK,CACnB,IAAME,EAASH,EAAI,kBAAkB,eAAe,EAChDG,GACH,aAAa,QAAQ,MAAOA,EAAO,QAAQ,YAAa,EAAE,CAAC,CAE7D,MAAWF,IAAW,KACrB,aAAa,WAAW,KAAK,GAE1BA,IAAW,KAAOA,IAAW,OAChCF,EAAO,WAAa,GACpBA,EAAO,QAAU,GAEnB,CAAC,EACDF,EAAK,iBAAiB,qBAAsB,SAAUC,EAAK,CACtD,aAAa,QAAQ,KAAK,IAC9BA,EAAI,OAAO,QAAQ,cAAgB,WAAa,aAAa,QAAQ,KAAK,GAAK,IAEhF,CAAC",
"names": ["shadow", "price", "multiplier", "major", "minor", "currency", "copyCss", "shadow", "css", "i", "styleSheet", "j", "sheet", "shadow", "copyCss", "body", "evt", "detail", "xhr", "status", "successful", "bearer"]
}

View File

@ -969,6 +969,11 @@ select {
padding: 1.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
@ -1039,6 +1044,11 @@ select {
color: rgb(17 24 39 / var(--tw-text-opacity));
}
.text-neutral-500 {
--tw-text-opacity: 1;
color: rgb(115 115 115 / var(--tw-text-opacity));
}
.text-red-700 {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
@ -1061,16 +1071,42 @@ select {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-200 {
transition-duration: 200ms;
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.hover\:text-neutral-700:hover {
--tw-text-opacity: 1;
color: rgb(64 64 64 / var(--tw-text-opacity));
}
.hover\:ease-in-out:hover {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.focus\:border-blue-500:focus {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.focus\:text-neutral-700:focus {
--tw-text-opacity: 1;
color: rgb(64 64 64 / var(--tw-text-opacity));
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
@ -1112,6 +1148,16 @@ select {
--tw-ring-offset-width: 2px;
}
.disabled\:text-black\/30:disabled {
color: rgb(0 0 0 / 0.3);
}
@media (prefers-reduced-motion: reduce) {
.motion-reduce\:transition-none {
transition-property: none;
}
}
@media (prefers-color-scheme: dark) {
.dark\:divide-gray-600 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
@ -1153,6 +1199,11 @@ select {
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.dark\:text-neutral-200 {
--tw-text-opacity: 1;
color: rgb(229 229 229 / var(--tw-text-opacity));
}
.dark\:text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
@ -1178,6 +1229,11 @@ select {
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:hover\:text-neutral-400:hover {
--tw-text-opacity: 1;
color: rgb(163 163 163 / var(--tw-text-opacity));
}
.dark\:hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
@ -1188,6 +1244,11 @@ select {
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.dark\:focus\:text-neutral-400:focus {
--tw-text-opacity: 1;
color: rgb(163 163 163 / var(--tw-text-opacity));
}
.dark\:focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
@ -1290,3 +1351,14 @@ select {
}
}
}
.\[\&\.active\]\:text-black\/90.active {
color: rgb(0 0 0 / 0.9);
}
@media (prefers-color-scheme: dark) {
.dark\:\[\&\.active\]\:text-neutral-400.active {
--tw-text-opacity: 1;
color: rgb(163 163 163 / var(--tw-text-opacity));
}
}

View File

@ -5,6 +5,8 @@ use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;
use uuid::Uuid;
pub static HEADER_NAME: &str = "Authorization";
pub trait Claims: PartialEq + DeserializeOwned + Serialize + Clone + Send + Sync + 'static {
fn jti(&self) -> uuid::Uuid;
}
@ -188,7 +190,7 @@ impl Extractor {
) -> Result<(), Error> {
let Some(authorisation_header) = req
.headers()
.get("Authorization")
.get(HEADER_NAME)
else {
return Ok(())
};

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use super::sea_orm_active_enums::Userrole;
use sea_orm::entity::prelude::*;
@ -66,7 +66,6 @@ impl PrimaryKeyTrait for PrimaryKey {
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Images,
ParkingSpaces,
}
@ -91,18 +90,11 @@ impl ColumnTrait for Column {
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Images => Entity::has_many(super::images::Entity).into(),
Self::ParkingSpaces => Entity::has_many(super::parking_spaces::Entity).into(),
}
}
}
impl Related<super::images::Entity> for Entity {
fn to() -> RelationDef {
Relation::Images.def()
}
}
impl Related<super::parking_spaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaces.def()

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use super::sea_orm_active_enums::ImageState;
use sea_orm::entity::prelude::*;
@ -59,9 +59,7 @@ impl PrimaryKeyTrait for PrimaryKey {
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Accounts,
}
pub enum Relation {}
impl ColumnTrait for Column {
type EntityName = Entity;
@ -80,18 +78,7 @@ impl ColumnTrait for Column {
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Accounts => Entity::belongs_to(super::accounts::Entity)
.from(Column::AccountId)
.to(super::accounts::Column::Id)
.into(),
}
}
}
impl Related<super::accounts::Entity> for Entity {
fn to() -> RelationDef {
Relation::Accounts.def()
panic!("No RelationDef")
}
}

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
pub mod prelude;

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use super::sea_orm_active_enums::ParkingSpaceState;
use sea_orm::entity::prelude::*;

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
pub use super::accounts::Entity as Accounts;
pub use super::images::Entity as Images;

View File

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;

View File

@ -10,7 +10,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use oswilno_session::{Authenticated, Claims};
use oswilno_view::{is_partial, Layout, LayoutOptions, Main};
use oswilno_view::{is_partial, Layout, Main, MainOpts};
pub fn mount(config: &mut ServiceConfig) {
config.service(
@ -45,7 +45,7 @@ async fn all_parking_spaces(
Layout {
main: Main {
body: parking_spaces,
opts: LayoutOptions::default(),
opts: MainOpts::default(),
},
}
.render()

View File

@ -9,14 +9,13 @@ use askama_actix::Template;
use autometrics::autometrics;
use garde::Validate;
use jsonwebtoken::*;
use oswilno_view::{Errors, Lang, Layout, LayoutOptions, Main, TranslationStorage};
use oswilno_view::{Errors, Lang, Layout, Main, MainOpts, TranslationStorage};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
use sea_orm::DatabaseConnection;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
mod extract_session;
mod hashing;
pub use oswilno_view::filters;
@ -164,7 +163,7 @@ async fn login_view(req: HttpRequest, t: Data<TranslationStorage>) -> HttpRespon
t: t.into_inner(),
errors: Errors::default(),
},
opts: LayoutOptions {
opts: MainOpts {
show: true,
search: None,
session: None,
@ -180,7 +179,7 @@ async fn login_view(req: HttpRequest, t: Data<TranslationStorage>) -> HttpRespon
t: t.into_inner(),
errors: Errors::default(),
},
opts: LayoutOptions {
opts: MainOpts {
show: true,
..Default::default()
},
@ -213,7 +212,7 @@ async fn login(
)
.await
{
Ok(res) => Ok(HttpResponse::Ok().json(res)),
Ok(res) => Ok(res),
Err(form) => Ok(HttpResponse::Ok().body(
(SignInPartialTemplate {
form,
@ -233,7 +232,7 @@ async fn login_inner(
db: Arc<DatabaseConnection>,
redis: Arc<SessionStorage<Claims>>,
errors: &mut Errors,
) -> Result<LoginResponse, SignInPayload> {
) -> Result<HttpResponse, SignInPayload> {
let iat = OffsetDateTime::now_utc().unix_timestamp() as usize;
let expires_at = OffsetDateTime::now_utc().add(jwt_ttl.0);
let exp = expires_at.unix_timestamp() as usize;
@ -255,7 +254,8 @@ async fn login_inner(
return Err(payload);
}
};
if hashing::verify(account.pass_hash.as_str(), payload.password.as_str()).is_err() {
if let Err(e) = hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
tracing::warn!("Hashing verification failed: {e}");
errors.push_global("Bad credentials");
return Err(payload);
}
@ -284,10 +284,16 @@ async fn login_inner(
return Err(payload);
}
};
Ok(LoginResponse {
bearer_token,
claims: jwt_claims,
})
Ok(HttpResponse::Ok()
.append_header((
actix_jwt_session::HEADER_NAME,
format!("Bearer {bearer_token}").as_str(),
))
.body(""))
// Ok(LoginResponse {
// bearer_token,
// claims: jwt_claims,
// })
}
#[autometrics]
@ -319,6 +325,8 @@ struct AccountInfo {
email: String,
#[garde(length(min = 8, max = 50), custom(is_strong_password))]
password: String,
#[garde(length(min = 8, max = 50), custom(is_strong_password), custom(check_pass_differ))]
password_confirmation: String,
}
#[get("/register")]
@ -332,7 +340,7 @@ async fn register_view(req: HttpRequest, t: Data<TranslationStorage>) -> HttpRes
lang: Lang::Pl,
errors: oswilno_view::Errors::default(),
},
opts: LayoutOptions {
opts: MainOpts {
show: true,
search: None,
session: None,
@ -348,7 +356,7 @@ async fn register_view(req: HttpRequest, t: Data<TranslationStorage>) -> HttpRes
lang: Lang::Pl,
errors: oswilno_view::Errors::default(),
},
opts: LayoutOptions::default(),
opts: MainOpts::default(),
},
}
.render()
@ -369,6 +377,7 @@ struct RegisterPartialTemplate {
#[autometrics]
#[post("/register")]
async fn register(
req: HttpRequest,
db: Data<DatabaseConnection>,
payload: Form<AccountInfo>,
t: Data<oswilno_view::TranslationStorage>,
@ -377,7 +386,7 @@ async fn register(
let t = t.into_inner();
let mut errors = oswilno_view::Errors::default();
Ok(
match register_internal(db.into_inner(), payload.into_inner(), &mut errors).await {
match register_internal(req, db.into_inner(), payload.into_inner(), &mut errors).await {
Ok(res) => res,
Err(p) => HttpResponse::BadRequest().body(
RegisterPartialTemplate {
@ -396,6 +405,7 @@ async fn register(
struct RegisterContext {
login_taken: bool,
email_taken: bool,
pass_differ: bool,
}
fn is_email_free(_value: &str, context: &RegisterContext) -> garde::Result {
@ -412,6 +422,15 @@ fn is_login_free(_value: &str, context: &RegisterContext) -> garde::Result {
}
static WEAK_PASS: &str = "is not strong enough";
fn check_pass_differ(_v: &str, ctx: &RegisterContext) -> garde::Result {
if ctx.pass_differ {
Err(garde::Error::new(DIFFER_PASS))
} else {
Ok(())
}
}
static DIFFER_PASS: &str = "passwords differ";
fn is_strong_password(value: &str, _context: &RegisterContext) -> garde::Result {
if !(8..50).contains(&value.len()) {
return Err(garde::Error::new(WEAK_PASS));
@ -434,6 +453,7 @@ fn is_strong_password(value: &str, _context: &RegisterContext) -> garde::Result
}
async fn register_internal(
_req: HttpRequest,
db: Arc<DatabaseConnection>,
p: AccountInfo,
errors: &mut oswilno_view::Errors,
@ -467,6 +487,7 @@ async fn register_internal(
if let Err(e) = p.validate(&RegisterContext {
login_taken,
email_taken,
pass_differ: p.password != p.password_confirmation,
}) {
errors.consume_garde(e);
return Err(p);
@ -499,7 +520,8 @@ async fn register_internal(
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/login"))
.json(EmptyResponse {}))
.append_header(("Accept", "text/html-partial"))
.body(""))
}
pub struct JwtSigningKeys {

View File

@ -45,6 +45,19 @@
<oswilno-error class="mb-2 mt-2">{{error|t(lang,t)}}</oswilno-error>
{% endfor %}
</div>
<div class="mb-4">
<label for="password_confirmation" class="block mb-2 text-sm text-gray-600">{{"Password confirmation"|t(lang,t)}}</label>
<input
id="password_confirmation"
name="password_confirmation"
type="password"
required
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
{% for error in errors.field("password_confirmation") %}
<oswilno-error class="mb-2 mt-2">{{error|t(lang,t)}}</oswilno-error>
{% endfor %}
</div>
<div class="mb-6">
<input
type="submit"

View File

@ -26,18 +26,28 @@ pub struct SessionOpts {
pub profile_image_url: Option<String>,
}
#[derive(Debug, Default)]
pub struct LayoutOptions {
#[derive(Debug)]
pub struct MainOpts {
pub show: bool,
pub search: Option<SearchOptions>,
pub session: Option<SessionOpts>,
}
impl Default for MainOpts {
fn default() -> Self {
Self {
show: true,
search: None,
session: None,
}
}
}
#[derive(Debug, askama_actix::Template)]
#[template(path = "../templates/main.html")]
pub struct Main<BodyTemplate: askama::Template> {
pub body: BodyTemplate,
pub opts: LayoutOptions,
pub opts: MainOpts,
}
#[derive(Debug, askama_actix::Template)]

View File

@ -1,7 +1,14 @@
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
<ul class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li>
<a href="#" class="block py-2 pl-3 pr-4 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500" aria-current="page">Home</a>
<a
href="#"
hx-replace-url="true"
class="block py-2 pl-3 pr-4 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500"
aria-current="page"
>
Home
</a>
</li>
<li>
<a href="#" class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700">About</a>

View File

@ -1,6 +1,6 @@
<div class="flex items-center md:order-2">
{% match opts.session %}
{% when Some with (session) %}
<div class="flex items-center md:order-2">
<button
type="button"
class="flex mr-3 text-sm bg-gray-800 rounded-full md:mr-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
@ -8,6 +8,7 @@
aria-expanded="false"
data-dropdown-toggle="user-dropdown"
data-dropdown-placement="bottom"
hx-replace-url="true"
>
<span class="sr-only">Open user menu</span>
{% match session.profile_image_url %}
@ -39,6 +40,31 @@
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
{% when None %}
<a
class="flex items-center px-2 text-neutral-500 transition duration-200 hover:text-neutral-700 hover:ease-in-out focus:text-neutral-700 disabled:text-black/30 motion-reduce:transition-none dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 [&.active]:text-black/90 dark:[&.active]:text-neutral-400"
href="/login"
hx-get='/login'
hx-target='main'
hx-headers='{"Accept":"text/html-partial"}'
data-te-dropdown-toggle-ref
aria-expanded="false"
hx-replace-url="true"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
stroke-width="1.5"
stroke="currentColor"
class="h-5 w-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
</a>
{% endmatch %}
</div>
{% when None %}
{% endmatch %}

View File

@ -2,11 +2,30 @@ import './elements/oswilno-price.js';
import './elements/oswilno-error.js';
import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");
document.body.addEventListener('htmx:beforeOnLoad', function (evt) {
const status = evt.detail.xhr.status;
const body = document.body;
body.addEventListener('htmx:beforeOnLoad', function (evt) {
const detail = evt.detail;
const xhr = detail.xhr;
const status = xhr.status;
const successful = detail.successful;
console.log(successful, xhr.getResponseHeader('Authorization'));
if (status === 200) {
const bearer = xhr.getResponseHeader('Authorization');
if (bearer) {
localStorage.setItem('jwt', bearer.replace(/^Bearer /i, ''));
}
} else if (status === 401) {
localStorage.removeItem('jwt');
}
if (status === 422 || status === 400) {
evt.detail.shouldSwap = true;
evt.detail.isError = false;
detail.shouldSwap = true;
detail.isError = false;
}
});
body.addEventListener('htmx:configRequest', function (evt) {
if (localStorage.getItem('jwt')) {
evt.detail.headers.Authorization = 'Bearer ' + (localStorage.getItem('jwt') || '');
}
});

View File

@ -1,19 +0,0 @@
[package]
name = "web"
version = "0.1.0"
edition = "2021"
[dependencies]
bincode = { version = "1.3.3", features = [] }
console_error_panic_hook = "0.1.7"
contract = { path = "../contract" }
serde = { version = "*", features = ['derive'] }
serde-wasm-bindgen = "*"
serde_json = { version = "*" }
sycamore = { version = "0.8.2", features = ['suspense'] }
sycamore-router = { version = "0.8" }
tracing = { version = "*" }
tracing-subscriber = { version = "*" }
tracing-subscriber-wasm = { version = "0.1.0" }
wasm_request = { version = "0.1.1" }
web-sys = { version = "0.3.61", features = ['Headers'] }

View File

@ -1,7 +0,0 @@
{
"dependencies": {
"@tailwindcss/line-clamp": "^0.4.4",
"tailwind": "^4.0.0",
"update-browserslist-db": "^1.0.11"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
use sycamore::prelude::*;
#[component]
pub fn Footer<G: Html>(cx: Scope) -> View<G> {
view! {
cx,
div(class="w-full h-64 xl:h-16 relative") {
footer(class="w-full absolute bottom-0") {
div(class="container mx-auto px-4") {
div(class="items-center xl:justify-between flex flex-wrap -mx-4") {
div(class="px-4 relative w-full xl:w-6/12 w-full sm:w-full") {
div(class="text-sm text-gray-200 text-center xl:text-left py-6") {
"Copyright © 2021"
a(href="https://ita-prog.pl", target="_blank", class="text-blueGray-100 font-semibold ml-1") {
"ITA Prog"
}
". All rights reserved."
}
}
div(class="px-4 relative w-full xl:w-6/12 w-full sm:w-full") {
ul(class="justify-center xl:justify-end mx-auto flex flex-wrap list-none pl-0 mb-0") {
li() {a(target = "__blank") {}}
li() {a(target = "__blank") {}}
li() {a(target = "__blank") {}}
}
}
}
}
}
}
}
}

View File

@ -1,64 +0,0 @@
use sycamore::prelude::*;
#[component]
pub fn Header<G: Html>(cx: Scope) -> View<G> {
view!(cx, div(class = "px-4 py-5 flex-auto ct-docs-frame") {
div(class = "relative flex flex-wrap justify-center bg-blueGray-100") {
div(class = "w-full h-16 relative") {
nav(class = "absolute w-full z-50 flex flex-wrap items-center justify-between px-2 py-3 mb-3 lg:bg-transparent bg-white") {
div(class = "flex container mx-auto flex flex-wrap items-center justify-between px-0 lg:px-4") {
a(
class = "text-sm font-bold leading-relaxed inline-flex items-center mr-4 py-2 whitespace-nowrap uppercase text-blueGray-500",
href = "/"
) {
img(class = "rounded-full mr-2", style = "width: 30px") {}
span() { "OS Wilno" }
button(class="ml-auto cursor-pointer text-xl leading-none px-3 py-1 border border-solid border-blueGray-400 rounded bg-transparent block outline-none focus:outline-none text-blueGray-300 lg:hidden", type="button") {
i(class="fas fa-bars")
}
div(class="items-center w-full lg:flex lg:w-auto flex-grow duration-300 transition-all ease-in-out hidden") {
ul(class="lg:items-center lg:ml-auto flex flex-wrap list-none pl-0 mb-0 flex flex-col list-none pl-0 mb-0 lg:flex-row") {
li {
a(class="hover:opacity-75 px-3 py-4 lg:py-2 flex items-center text-xs uppercase font-bold transition-all duration-150 ease-in-out") {
"Usługi"
}
}
li {
a(class="hover:opacity-75 px-3 py-4 lg:py-2 flex items-center text-xs uppercase font-bold transition-all duration-150 ease-in-out") {
"Garaż"
}
}
li(class = "relative") {
a(class="hover:opacity-75 px-3 py-4 lg:py-2 flex items-center text-xs uppercase font-bold transition-all duration-150 ease-in-out text-blueGray-800", href="") {
"Demo Pages"
i(class="ml-1 fas fa-caret-down transition-all duration-200 ease-in-out transform")
}
div(class="hidden z-50") {
div(class="origin-top-right bg-white text-base float-left p-2 border list-none text-left rounded-lg shadow-lg min-w-48 transition-all duration-100 ease-in-out transform scale-95 opacity-0 absolute") {
span(class="uppercase font-bold text-xs px-3 pt-4 block w-full whitespace-nowrap bg-transparent text-blueGray-400") {
"Group 1"
}
a(class="text-sm px-3 py-2 block w-full whitespace-nowrap bg-transparent hover:bg-blueGray-100 rounded transition-all duration-100") {
"Demo page 1"
}
a(class="text-sm px-3 py-2 block w-full whitespace-nowrap bg-transparent hover:bg-blueGray-100 rounded transition-all duration-100") {
"Demo page 2"
}
span(class="uppercase font-bold text-xs px-3 pt-4 block w-full whitespace-nowrap bg-transparent text-blueGray-400") { "Group 2" }
}
}
}
li(class = "relative") {
a(class="hover:opacity-75 px-3 py-4 lg:py-2 flex items-center text-xs uppercase font-bold transition-all duration-150 ease-in-out", href = "/account") {
"Account"
}
}
}
}
}
}
}
}
}
})
}

View File

@ -1,7 +0,0 @@
mod card;
mod footer;
mod header;
pub use card::*;
pub use footer::*;
pub use header::*;

View File

@ -1,23 +0,0 @@
mod components;
mod pages;
use pages::AppRoutes;
use sycamore::prelude::*;
use tracing_subscriber_wasm::MakeConsoleWriter;
fn main() {
console_error_panic_hook::set_once();
tracing_subscriber::fmt::fmt()
.with_ansi(false)
.with_writer(MakeConsoleWriter::default().map_trace_level_to(tracing::Level::DEBUG))
.without_time()
.init();
sycamore::render(|cx| {
view! { cx,
main() {
AppRoutes()
}
}
});
}

View File

@ -1,7 +0,0 @@
pub mod details;
pub mod register;
pub mod sign_in;
use details::*;
use register::*;
use sign_in::*;

View File

@ -1,105 +0,0 @@
use sycamore::prelude::*;
#[component]
pub fn SignIn<G: Html>(cx: Scope) -> View<G> {
let name = create_signal(cx, "".to_string());
let pass = create_signal(cx, "".to_string());
let agree = create_signal(cx, false);
view!(
cx,
div(class="flex content-center items-center justify-center h-full") {
div(class="w-full lg:w-5/12 px-4") {
div(class="relative flex flex-col w-full mb-6 shadow-lg rounded-lg bg-white") {
div(class="mb-0 px-6 py-6") {
div(class="text-center mb-3") {
h6(class="text-blueGray-500 text-sm font-bold") { "Sign up with" }
}
div(class="text-center") {
a(
class="inline-block outline-none focus:outline-none align-middle transition-all duration-150 ease-in-out uppercase border border-solid font-bold last:mr-0 mr-2 text-white bg-github-regular border-github-regular active:bg-github-active active:border-github-active text-xs px-3 py-2 shadow hover:shadow-md rounded-md",
style="background-color: rgba(34, 34, 34, var(--tw-bg-opacity))"
) {
i( class="mr-1 fab fa-solid fa-github")
" github"
}
a(
class="inline-block outline-none focus:outline-none align-middle transition-all duration-150 ease-in-out uppercase border border-solid font-bold last:mr-0 mr-2 text-white bg-facebook-regular border-facebook-regular active:bg-facebook-active active:border-facebook-active text-xs px-3 py-2 shadow hover:shadow-md rounded-md",
style="background-color: rgba(59, 89, 153, var(--tw-bg-opacity))"
) {
i(class="mr-1 fab fa-solid fa-facebook")
" facebook"
}
}
hr(class="mt-6 border-b-1 border-blueGray-200")
}
div(class="flex-auto px-4 lg:px-10 py-10 pt-0") {
div(class="text-blueGray-500 text-center mb-3 font-bold") {
small { "Or sign up with credentials" }
}
form(data-bitwarden-watching="1") {
div(class="relative w-full") {
label(
class="block uppercase text-blueGray-500 text-xs font-bold mb-2 ml-1"
) {
"Name"
}
div(class="mb-3 pt-0") {
input(
placeholder="Name",
type="text",
bind:value=name,
class="border-blueGray-300 px-3 py-2 text-sm w-full placeholder-blueGray-200 text-blueGray-700 relative bg-white rounded-md outline-none focus:ring focus:ring-lightBlue-500 focus:ring-1 focus:border-lightBlue-500 border border-solid transition duration-200"
)
}
}
div(
class="relative w-full"
) {
label(
class="block uppercase text-blueGray-500 text-xs font-bold mb-2 ml-1"
) {
"Password"
}
div(class="mb-3 pt-0") {
input(
placeholder="Password",
type="password",
bind:value=pass,
class="border-blueGray-300 px-3 py-2 text-sm w-full placeholder-blueGray-200 text-blueGray-700 relative bg-white rounded-md outline-none focus:ring focus:ring-lightBlue-500 focus:ring-1 focus:border-lightBlue-500 border border-solid transition duration-200 "
)
}
}
div(
class="mt-2 inline-block"
) {
label(
class="inline-flex items-center cursor-pointer"
) {
input(
type="checkbox",
bind:checked=agree,
class="form-checkbox appearance-none ml-1 w-5 h-5 ease-linear transition-all duration-150 border border-blueGray-300 rounded checked:bg-blueGray-700 checked:border-blueGray-700 focus:border-blueGray-300"
)
span(class="ml-2 text-sm font-semibold text-blueGray-500") {
"I agree with the Privacy Policy"
}
}
}
div(class="text-center mt-5") {
a(
href="/register",
class="inline-block outline-none focus:outline-none align-middle transition-all duration-150 ease-in-out uppercase border border-solid font-bold last:mr-0 mr-2 text-white bg-blueGray-800 border-blueGray-800 active:bg-blueGray-900 active:border-blueGray-900 text-sm px-6 py-2 shadow hover:shadow-lg rounded-md w-full text-center",
style="background-color: rgba(30, 41, 59, var(--tw-bg-opacity))"
) {
"Create account"
}
}
}
}
}
}
}
)
}

View File

@ -1,103 +0,0 @@
use contract::LocalBusinessItem;
use sycamore::prelude::*;
#[derive(Prop)]
pub struct LocalBusinessCardProps {
name: String,
desc: String,
items: Vec<LocalBusinessItem>,
}
#[component]
pub fn LocalBusinessCard<G: Html>(cx: Scope<'_>, props: LocalBusinessCardProps) -> View<G> {
let items = create_signal(cx, props.items);
view! {
cx,
div(class = "px-4 relative w-full md:w-4/12") {
div(class = "mb-6 text-center shadow-lg rounded-lg relative flex flex-col bg-white p-6 w-full mb-6") {
div(class = "py-6 flex-auto") {
div(class = "shadow-lg mt-6 rounded-full my-6 mx-auto w-100-px p-6 bg-white") {
img(class = "mx-auto", src = "data:image/png;base64,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") {}
}
h4(class = "text-2xl font-bold leading-tight mt-0 mb-2") { (props.name) }
p(class="text-blueGray-500 px-8 line-clamp-2") { (props.desc) }
div(class = "flex justify-center mt-8 mb-2 text-blueGray-400") {
div(class = "flex items-center") {
Indexed(
iterable=items,
view=|cx, item| view! { cx,
BusinessItem(
url = item.picture_url,
name = item.name
)
}
)
}
}
Rating(rating = 37)
}
}
}
}
}
#[derive(Prop)]
pub struct BusinessItemProps {
name: String,
url: String,
}
#[component]
pub fn BusinessItem<G: Html>(cx: Scope<'_>, props: BusinessItemProps) -> View<G> {
view!(
cx,
a(
class="text-white bg-blueGray-500 inline-flex items-center justify-center shadow-lg rounded rounded-full relative border-2 border-white -ml-4 hover:z-1 w-10 h-10",
title = props.name
) {
img(class="rounded-full w-full", src=(props.url)) {}
div(class = "hidden") {
div(class = "border-0 mb-3 block z-50 font-normal leading-normal text-sm text-left no-underline break-words rounded") {
div(class = "py-1 px-2 text-center rounded text-white bg-black") { "Photo" }
}
}
}
)
}
#[derive(Prop)]
struct RatingProps {
rating: u8,
}
#[component]
fn Rating<G: Html>(cx: Scope, props: RatingProps) -> View<G> {
let rating = props.rating;
let has_half = rating % 10 != 0;
let full = rating / 10;
let empty = if has_half { 4 - full } else { 5 - full };
let full_it = create_signal(cx, (0..full).collect::<Vec<_>>());
let empty_it = create_signal(cx, (0..empty).collect::<Vec<_>>());
view! {
cx,
div(class="w-full") {
div(class="text-orange-500") {
Indexed(
iterable = full_it,
view = |cx, _| view!(cx, i(class="mr-1 fas fa-star"))
)
(if has_half {
view!(cx, i(class="mr-1 fas fa-star-half-alt"))
} else {
view!(cx, )
})
Indexed(
iterable = empty_it,
view = |cx, _| view!(cx, i(class="mr-1 fa fa-regular fa-star"))
)
a(href="https://www.creative-tim.com", class="inline text-sm ml-1 text-blueGray-700 hover:text-blueGray-500") { "76 customer reviews" }
}
}
}
}

View File

@ -1,89 +0,0 @@
use contract::businesses::BusinessList;
use contract::*;
use sycamore::prelude::*;
use sycamore::suspense::Suspense;
use tracing::info;
use super::local_business::LocalBusinessCard;
#[component]
async fn LocalBusinessesList<G: Html>(cx: Scope<'_>) -> View<G> {
let req = wasm_request::get_options::<Vec<LocalBusiness>>(
"/api/local-businesses",
wasm_request::Method::Get,
Some({
let h = web_sys::Headers::new().unwrap();
h.append("Accept", "application/json").unwrap();
h
}),
None,
);
let payload = wasm_request::request(req).await.unwrap_or_default();
let page = serde_wasm_bindgen::from_value::<BusinessList>(payload).unwrap_or_default();
info!("{page:#?}");
let businesses = {
let businesses = page.businesses.clone();
create_signal(cx, businesses)
};
let search = create_signal(cx, "".to_string());
let visible = create_memo(cx, || {
let s = &*search.get();
let s = s.trim();
if s.is_empty() {
let b = businesses.get();
b.as_slice().to_vec()
} else {
businesses
.get()
.iter()
.filter(|b| b.name.contains(s) || b.description.contains(s))
.cloned()
.collect::<Vec<_>>()
}
});
view! {
cx,
div(class = "container mx-auto px-4") {
div(class = "items-center w-full lg:flex lg:w-auto flex-grow duration-300 transition-all ease-in-out lg:h-auto-important hidden") {
form(class = "flex flex-row flex-wrap items-center ml-auto mr-3 mt-4") {
div(class = "hover:opacity-75 px-3 py-4 lg:py-2 flex items-center text-xs uppercase font-bold transition-all duration-150 ease-in-out text-blueGray-500 pr-4") {
i(class = "lg:text-blueGray-300 text-blueGray-500 far text-lg leading-lg mr-2 fa-solid fa-magnifying-glass")
}
div(class = "mb-3 pt-0") {
input(
id = "search-local-business",
bind:value = search,
type = "search",
placeholder = "Znajdź",
class = "border-transparent shadow px-3 py-2 text-sm w-full placeholder-blueGray-200 text-blueGray-700 relative bg-white rounded-md outline-none focus:ring focus:ring-lightBlue-500 focus:ring-1 focus:border-lightBlue-500 border border-solid transition duration-200"
)
}
}
}
div(class = "mb-12 flex flex-wrap -mx-4") {
Indexed(
iterable = visible,
view = |cx, business| view! { cx,
LocalBusinessCard(
name = business.name,
desc = business.description,
items = business.items
)
}
)
}
}
}
}
#[component]
pub fn LocalBusinesses<G: Html>(cx: Scope) -> View<G> {
view!(
cx,
Suspense(fallback = view! { cx, "Loading..." }) {
LocalBusinessesList()
}
)
}

View File

@ -1,5 +0,0 @@
mod local_business;
mod local_businesses;
use local_business::*;
pub use local_businesses::*;

View File

@ -1,48 +0,0 @@
use account::sign_in::SignIn;
use local_businesses::LocalBusinesses;
use sycamore::prelude::*;
use sycamore_router::*;
use crate::components::{Footer, Header};
mod account;
mod local_businesses;
#[derive(Route)]
enum Routes {
#[to("/")]
Home,
#[to("/account")]
Account,
#[not_found]
NotFound,
}
#[component]
pub fn AppRoutes<G: Html>(cx: Scope) -> View<G> {
view! {
cx,
Header()
Router(
integration=HistoryIntegration::new(),
view=|cx, route: &ReadSignal<Routes>| {
view! {
cx,
article(class="w-full") {
(match route.get().as_ref() {
Routes::Home => view! { cx,
LocalBusinesses()
},
Routes::Account => view! { cx,
SignIn()
},
Routes::NotFound => view! { cx,
"404 Not Found"
},
})
}
}
}
)
Footer()
}
}