Merge
This commit is contained in:
commit
335a84838f
1384
Cargo.lock
generated
1384
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
7
assets/build.js
Normal file
7
assets/build.js
Normal file
@ -0,0 +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=`
|
||||
<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)});})();
|
||||
//# sourceMappingURL=build.js.map
|
7
assets/build.js.map
Normal file
7
assets/build.js.map
Normal file
@ -0,0 +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"]
|
||||
}
|
847
assets/style.css
Normal file
847
assets/style.css
Normal file
@ -0,0 +1,847 @@
|
||||
/*
|
||||
! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-feature-settings: inherit;
|
||||
/* 1 */
|
||||
font-variation-settings: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset default styling for dialogs.
|
||||
*/
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
border-color: #6b7280;
|
||||
border-width: 1px;
|
||||
border-radius: 0px;
|
||||
padding-top: 0.5rem;
|
||||
padding-right: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
}
|
||||
|
||||
[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: #2563eb;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input::placeholder,textarea::placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
[multiple],[size]:where(select:not([size="1"])) {
|
||||
background-image: initial;
|
||||
background-position: initial;
|
||||
background-repeat: unset;
|
||||
background-size: initial;
|
||||
padding-right: 0.75rem;
|
||||
-webkit-print-color-adjust: unset;
|
||||
print-color-adjust: unset;
|
||||
}
|
||||
|
||||
[type='checkbox'],[type='radio'] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-origin: border-box;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
color: #2563eb;
|
||||
background-color: #fff;
|
||||
border-color: #6b7280;
|
||||
border-width: 1px;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
}
|
||||
|
||||
[type='checkbox'] {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
[type='radio'] {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
[type='checkbox']:focus,[type='radio']:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
||||
--tw-ring-offset-width: 2px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: #2563eb;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
[type='checkbox']:checked,[type='radio']:checked {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
[type='checkbox']:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
[type='radio']:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[type='checkbox']:indeterminate {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
[type='file'] {
|
||||
background: unset;
|
||||
border-color: inherit;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: unset;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
[type='file']:focus {
|
||||
outline: 1px solid ButtonText;
|
||||
outline: 1px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.bg-cyan-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(8 145 178 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(185 28 28 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.focus\:outline-none:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.focus\:ring-2:focus {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-cyan-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(6 182 212 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-offset-2:focus {
|
||||
--tw-ring-offset-width: 2px;
|
||||
}
|
@ -11,7 +11,7 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||
oswilno-contract = { path = "../oswilno-contract" }
|
||||
sea-orm = { version = "0.11", features = ["runtime-actix-rustls", "sqlx-postgres", "postgres-array", "sqlx"] }
|
||||
sea-orm = { version = "0.11.3", features = ["runtime-actix-rustls", "sqlx-postgres", "postgres-array", "sqlx"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
|
36
crates/migration/src/m20230805_000001_add_email.rs
Normal file
36
crates/migration/src/m20230805_000001_add_email.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use crate::m20220101_000001_create_table::Account;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||
let table = Table::alter()
|
||||
.table(Account::Accounts)
|
||||
.add_column(
|
||||
ColumnDef::new(Account::Email)
|
||||
.string()
|
||||
.unique_key()
|
||||
.default("filler@example.com")
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
m.alter_table(table).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
|
||||
m.alter_table(
|
||||
Table::alter()
|
||||
.table(Account::Accounts)
|
||||
.drop_column(Account::Email)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@ oswilno-config = { path = "../oswilno-config" }
|
||||
oswilno-parking-space = { path = "../oswilno-parking-space" }
|
||||
oswilno-session = { path = "../oswilno-session" }
|
||||
oswilno-view = { path = "../oswilno-view" }
|
||||
redis = { version = "0.17" }
|
||||
redis-async-pool = "0.2.4"
|
||||
sea-orm = { version = "0.11", features = ["postgres-array", "runtime-actix-rustls", "sqlx-postgres"] }
|
||||
serde = { version = "1.0.175", features = ["derive"] }
|
||||
serde_json = "1.0.103"
|
||||
|
@ -29,8 +29,21 @@ async fn main() -> std::io::Result<()> {
|
||||
.idle_timeout(Duration::from_secs(8))
|
||||
.sqlx_logging(true);
|
||||
let conn: sea_orm::DatabaseConnection = sea_orm::Database::connect(db_opts).await.unwrap();
|
||||
|
||||
let redis = {
|
||||
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
||||
RedisPool::new(
|
||||
RedisConnectionManager::new(
|
||||
redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
||||
true,
|
||||
None,
|
||||
),
|
||||
5,
|
||||
)
|
||||
};
|
||||
|
||||
let mut l10n = oswilno_view::TranslationStorage::new();
|
||||
let session_config = oswilno_session::SessionConfigurator::new();
|
||||
let session_config = oswilno_session::SessionConfigurator::new(redis.clone());
|
||||
session_config.translations(&mut l10n);
|
||||
|
||||
HttpServer::new(move || {
|
||||
@ -39,6 +52,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.wrap(middleware::Logger::default())
|
||||
.wrap(session_config.factory())
|
||||
.app_data(Data::new(conn.clone()))
|
||||
.app_data(Data::new(redis.clone()))
|
||||
.app_data(Data::new(l10n.clone()))
|
||||
.configure(oswilno_parking_space::mount)
|
||||
.configure(oswilno_admin::mount)
|
||||
|
@ -11,12 +11,15 @@ argon2 = "0.5.1"
|
||||
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
|
||||
askama_actix = { version = "0.14.0" }
|
||||
autometrics = { version = "0.5.0", features = ["tracing", "tracing-subscriber", "thiserror"] }
|
||||
bincode = "1.3.3"
|
||||
futures = { version = "0.3.28", features = ["thread-pool"] }
|
||||
garde = { version = "0.14.0", features = ["derive"] }
|
||||
jsonwebtoken = "8.3.0"
|
||||
oswilno-contract = { path = "../oswilno-contract" }
|
||||
oswilno-view = { path = "../oswilno-view" }
|
||||
rand = "0.8.5"
|
||||
redis = { version = "0.17" }
|
||||
redis-async-pool = "0.2.4"
|
||||
ring = "0.16.20"
|
||||
sea-orm = { version = "0.11", features = ["postgres-array", "runtime-actix-rustls", "sqlx-postgres", "macros", "sqlx"] }
|
||||
serde = { version = "1.0.180", features = ["derive"] }
|
||||
|
@ -4,14 +4,14 @@ use std::sync::Arc;
|
||||
use actix_jwt_authc::*;
|
||||
use actix_web::web::{Data, Form, ServiceConfig};
|
||||
use actix_web::{get, post, HttpResponse};
|
||||
use askama_actix::Template;
|
||||
use askama_actix::{Template, TemplateToResponse as _};
|
||||
use autometrics::autometrics;
|
||||
use futures::channel::{mpsc, mpsc::Sender};
|
||||
use futures::stream::Stream;
|
||||
use futures::SinkExt;
|
||||
use garde::Validate;
|
||||
use jsonwebtoken::*;
|
||||
use oswilno_view::{Lang, TranslationStorage};
|
||||
use oswilno_view::{Errors, Lang, TranslationStorage};
|
||||
use ring::rand::SystemRandom;
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use sea_orm::DatabaseConnection;
|
||||
@ -115,7 +115,7 @@ impl SessionConfigurator {
|
||||
.done();
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
pub fn new(redis: redis_async_pool::RedisPool) -> Self {
|
||||
let jwt_ttl = JWTTtl(31.days());
|
||||
let jwt_signing_keys = JwtSigningKeys::generate().unwrap();
|
||||
let validator = Validation::new(JWT_SIGNING_ALGO);
|
||||
@ -125,7 +125,7 @@ impl SessionConfigurator {
|
||||
jwt_validator: validator,
|
||||
jwt_session_key: None,
|
||||
};
|
||||
let (invalidated_jwts_store, stream) = InvalidatedJWTStore::new_with_stream();
|
||||
let (invalidated_jwts_store, stream) = InvalidatedJWTStore::new_with_stream(redis);
|
||||
let auth_middleware_factory =
|
||||
AuthenticateMiddlewareFactory::<Claims>::new(stream, auth_middleware_settings.clone());
|
||||
|
||||
@ -144,6 +144,7 @@ struct SignInTemplate {
|
||||
form: SignInPayload,
|
||||
lang: Lang,
|
||||
t: Arc<TranslationStorage>,
|
||||
errors: Errors,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -152,20 +153,13 @@ struct SignInPartialTemplate {
|
||||
form: SignInPayload,
|
||||
lang: Lang,
|
||||
t: Arc<TranslationStorage>,
|
||||
errors: Errors,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Default)]
|
||||
pub struct SignInPayload {
|
||||
#[serde(skip, default)]
|
||||
errors: Vec<String>,
|
||||
|
||||
login: String,
|
||||
#[serde(skip, default)]
|
||||
login_errors: Vec<String>,
|
||||
|
||||
password: String,
|
||||
#[serde(skip, default)]
|
||||
password_errors: Vec<String>,
|
||||
}
|
||||
|
||||
#[get("/login")]
|
||||
@ -174,6 +168,7 @@ async fn login_view(t: Data<TranslationStorage>) -> SignInTemplate {
|
||||
form: SignInPayload::default(),
|
||||
lang: Lang::Pl,
|
||||
t: t.into_inner(),
|
||||
errors: Errors::default(),
|
||||
}
|
||||
}
|
||||
#[get("/p/login")]
|
||||
@ -182,6 +177,7 @@ async fn login_partial_view(t: Data<TranslationStorage>) -> SignInPartialTemplat
|
||||
form: SignInPayload::default(),
|
||||
lang: Lang::Pl,
|
||||
t: t.into_inner(),
|
||||
errors: Errors::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,25 +190,37 @@ async fn login(
|
||||
payload: Form<SignInPayload>,
|
||||
t: Data<oswilno_view::TranslationStorage>,
|
||||
lang: Lang,
|
||||
redis: Data<redis_async_pool::RedisPool>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let t = t.into_inner();
|
||||
match login_inner(jwt_encoding_key, jwt_ttl, db, payload).await {
|
||||
let mut errors = Errors::default();
|
||||
match login_inner(
|
||||
jwt_encoding_key,
|
||||
jwt_ttl,
|
||||
payload.into_inner(),
|
||||
db.into_inner(),
|
||||
redis.into_inner(),
|
||||
&mut errors,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => Ok(HttpResponse::Ok().json(res)),
|
||||
Err(form) => {
|
||||
Ok(HttpResponse::Ok().body(SignInPartialTemplate { form, lang, t }.render().unwrap()))
|
||||
}
|
||||
Err(form) => Ok(HttpResponse::Ok().body(
|
||||
(SignInPartialTemplate { form, lang, t, errors })
|
||||
.render()
|
||||
.unwrap(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_inner(
|
||||
jwt_encoding_key: Data<EncodingKey>,
|
||||
jwt_ttl: Data<JWTTtl>,
|
||||
db: Data<DatabaseConnection>,
|
||||
payload: Form<SignInPayload>,
|
||||
payload: SignInPayload,
|
||||
db: Arc<DatabaseConnection>,
|
||||
redis: Arc<redis_async_pool::RedisPool>,
|
||||
errors: &mut Errors,
|
||||
) -> Result<LoginResponse, SignInPayload> {
|
||||
let db = db.into_inner();
|
||||
let mut payload = payload.into_inner();
|
||||
|
||||
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;
|
||||
@ -225,17 +233,17 @@ async fn login_inner(
|
||||
{
|
||||
Ok(Some(a)) => a,
|
||||
Ok(None) => {
|
||||
payload.errors.push("Bad credentials".into());
|
||||
errors.push_global("Bad credentials");
|
||||
return Err(payload);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to find account: {e}");
|
||||
payload.errors.push("Bad credentials".into());
|
||||
errors.push_global("Bad credentials");
|
||||
return Err(payload);
|
||||
}
|
||||
};
|
||||
if hashing::verify(account.pass_hash.as_str(), payload.password.as_str()).is_err() {
|
||||
payload.errors.push("Bad credentials".into());
|
||||
errors.push_global("Bad credentials");
|
||||
return Err(payload);
|
||||
}
|
||||
|
||||
@ -252,9 +260,29 @@ async fn login_inner(
|
||||
&jwt_encoding_key,
|
||||
)
|
||||
.map_err(|_| {
|
||||
payload.errors.push("Bad credentials".into());
|
||||
payload
|
||||
errors.push_global("Bad credentials");
|
||||
payload.clone()
|
||||
})?;
|
||||
let Ok(bin_value) = bincode::serialize(&jwt_claims) else {
|
||||
errors.push_global("Failed to sign in. Please try later");
|
||||
return Err(payload.clone());
|
||||
};
|
||||
{
|
||||
use redis::AsyncCommands;
|
||||
|
||||
let Ok(mut conn) = redis.get().await else {
|
||||
errors.push_global("Failed to sign in. Please try later");
|
||||
return Err(payload);
|
||||
};
|
||||
if let Err(e) = conn
|
||||
.set::<'_, _, _, String>(jwt_claims.jwt_id.as_bytes(), bin_value)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to set sign-in claims in redis: {e}");
|
||||
errors.push_global("Failed to sign in. Please try later");
|
||||
return Err(payload);
|
||||
}
|
||||
}
|
||||
Ok(LoginResponse {
|
||||
bearer_token: jwt_token,
|
||||
claims: jwt_claims,
|
||||
@ -465,12 +493,15 @@ async fn register_internal(
|
||||
#[derive(Clone)]
|
||||
struct InvalidatedJWTStore {
|
||||
// store: Arc<DashSet<JWT>>,
|
||||
redis: redis_async_pool::RedisPool,
|
||||
tx: Arc<Mutex<Sender<InvalidatedTokensEvent>>>,
|
||||
}
|
||||
|
||||
impl InvalidatedJWTStore {
|
||||
/// Returns a [InvalidatedJWTStore] with a Stream of [InvalidatedTokensEvent]s
|
||||
fn new_with_stream() -> (
|
||||
fn new_with_stream(
|
||||
redis: redis_async_pool::RedisPool,
|
||||
) -> (
|
||||
InvalidatedJWTStore,
|
||||
impl Stream<Item = InvalidatedTokensEvent>,
|
||||
) {
|
||||
@ -480,6 +511,7 @@ impl InvalidatedJWTStore {
|
||||
(
|
||||
InvalidatedJWTStore {
|
||||
// store: invalidated,
|
||||
redis,
|
||||
tx: tx_to_hold,
|
||||
},
|
||||
rx,
|
||||
|
@ -1,20 +1,19 @@
|
||||
<section id="main-view" class="min-h-screen flex items-center justify-center">
|
||||
<section class="max-w-md w-full p-6 bg-white rounded-lg shadow-lg">
|
||||
{% for error in form.errors %}
|
||||
{% for error in errors.global() %}
|
||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
||||
{% endfor %}
|
||||
<form hx-post="/login" hx-target="#main-view">
|
||||
<div class="mb-4">
|
||||
<label for="login" class="block mb-2 text-sm text-gray-600">Login</label>
|
||||
<input id="login" name="login" value="{{ form.login }}" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500" />
|
||||
{% for error in form.login_errors %}
|
||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
||||
{% for error in errors.field("login") %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block mb-2 text-sm text-gray-600">Password</label>
|
||||
<input id="password" name="password" type="password" value="" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500" />
|
||||
{% for error in form.password_errors %}
|
||||
{% for error in errors.field("password") %}
|
||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
80
crates/oswilno-view/src/lang.rs
Normal file
80
crates/oswilno-view/src/lang.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use actix_web::error::Error;
|
||||
use actix_web::HttpRequest;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug, Default)]
|
||||
pub enum Lang {
|
||||
#[default]
|
||||
Pl,
|
||||
En,
|
||||
}
|
||||
|
||||
impl FromStr for Lang {
|
||||
type Err = ();
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match value {
|
||||
"pl" => Self::Pl,
|
||||
"en" => Self::En,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtractLangFuture {
|
||||
req: Option<HttpRequest>,
|
||||
}
|
||||
|
||||
impl Future for ExtractLangFuture {
|
||||
type Output = Result<Lang, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
let default_lang = Lang::Pl;
|
||||
|
||||
let Some(req) = this.req.as_ref() else {
|
||||
return Poll::Ready(Ok(default_lang));
|
||||
};
|
||||
|
||||
let Some(v) = req.headers().get("Accept-Language") else {
|
||||
return Poll::Ready(Ok(default_lang));
|
||||
};
|
||||
|
||||
tracing::trace!("Accept-Language value -> {v:?}");
|
||||
let Ok(value) = v.to_str() else {
|
||||
return Poll::Ready(Ok(default_lang));
|
||||
};
|
||||
tracing::trace!("Accept-Language str {value:?}");
|
||||
for lang in value.split(',') {
|
||||
let l = lang.trim();
|
||||
let Some(lang) = l.split(';').next() else {
|
||||
tracing::trace!("Failed to split {l:?}");
|
||||
continue;
|
||||
};
|
||||
let lang = lang.trim();
|
||||
tracing::trace!("trying lang {lang:?}");
|
||||
if let Ok(lang) = lang.parse::<Lang>() {
|
||||
tracing::trace!("found lang {lang:?}");
|
||||
return Poll::Ready(Ok(lang));
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(Lang::Pl))
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::FromRequest for Lang {
|
||||
type Error = actix_web::error::Error;
|
||||
type Future = ExtractLangFuture;
|
||||
|
||||
fn from_request(
|
||||
req: &actix_web::HttpRequest,
|
||||
_payload: &mut actix_web::dev::Payload,
|
||||
) -> Self::Future {
|
||||
ExtractLangFuture {
|
||||
req: Some(req.clone()),
|
||||
}
|
||||
}
|
||||
}
|
19
crates/web/Cargo.toml
Normal file
19
crates/web/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[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'] }
|
7
crates/web/package.json
Normal file
7
crates/web/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"tailwind": "^4.0.0",
|
||||
"update-browserslist-db": "^1.0.11"
|
||||
}
|
||||
}
|
1969
crates/web/pnpm-lock.yaml
Normal file
1969
crates/web/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
32
crates/web/src/components/footer.rs
Normal file
32
crates/web/src/components/footer.rs
Normal file
@ -0,0 +1,32 @@
|
||||
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") {}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
crates/web/src/components/header.rs
Normal file
64
crates/web/src/components/header.rs
Normal file
@ -0,0 +1,64 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
7
crates/web/src/components/mod.rs
Normal file
7
crates/web/src/components/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod card;
|
||||
mod footer;
|
||||
mod header;
|
||||
|
||||
pub use card::*;
|
||||
pub use footer::*;
|
||||
pub use header::*;
|
23
crates/web/src/main.rs
Normal file
23
crates/web/src/main.rs
Normal file
@ -0,0 +1,23 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
0
crates/web/src/pages/account/details.rs
Normal file
0
crates/web/src/pages/account/details.rs
Normal file
7
crates/web/src/pages/account/mod.rs
Normal file
7
crates/web/src/pages/account/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod details;
|
||||
pub mod register;
|
||||
pub mod sign_in;
|
||||
|
||||
use details::*;
|
||||
use register::*;
|
||||
use sign_in::*;
|
0
crates/web/src/pages/account/register.rs
Normal file
0
crates/web/src/pages/account/register.rs
Normal file
105
crates/web/src/pages/account/sign_in.rs
Normal file
105
crates/web/src/pages/account/sign_in.rs
Normal file
@ -0,0 +1,105 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
103
crates/web/src/pages/local_businesses/local_business.rs
Normal file
103
crates/web/src/pages/local_businesses/local_business.rs
Normal file
@ -0,0 +1,103 @@
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
89
crates/web/src/pages/local_businesses/local_businesses.rs
Normal file
89
crates/web/src/pages/local_businesses/local_businesses.rs
Normal file
@ -0,0 +1,89 @@
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
5
crates/web/src/pages/local_businesses/mod.rs
Normal file
5
crates/web/src/pages/local_businesses/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod local_business;
|
||||
mod local_businesses;
|
||||
|
||||
use local_business::*;
|
||||
pub use local_businesses::*;
|
48
crates/web/src/pages/mod.rs
Normal file
48
crates/web/src/pages/mod.rs
Normal file
@ -0,0 +1,48 @@
|
||||
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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user