UI tuning

This commit is contained in:
Adrian Woźniak 2022-07-29 16:08:59 +02:00
parent a9669432ec
commit f91b4fb4f9
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
50 changed files with 936 additions and 534 deletions

View File

@ -2,6 +2,34 @@
@import 'noto-sans.css';
@import 'beam-weapon.css';
/* latin-ext */
@font-face {
font-family: 'Cardo';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cardo/v19/wlp_gwjKBV1pqhv23IEp2A.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cardo';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cardo/v19/wlp_gwjKBV1pqhv43IE.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Garamond Premier Pro Display';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/assets/fonts/Garamond Premier Pro Display.otf') format('otf');
}
* {
--hover-color: #f18902;
--border-slim-color: #495057;
@ -9,6 +37,8 @@
--red-color: #9D0208;
--orange-color: #E85D04;
--blue-color: #023e8a;
--ast-border-color: #dddddd;
}
header > h1 {
@ -16,23 +46,6 @@ header > h1 {
}
@media (min-width: 1200px) {
header > h1 {
font-family: 'Beam Weapon', sans-serif;
font-weight: bold;
text-shadow: 2px 1px 2px #fff, -1px -1px 2px #fff;
display: block;
font-size: 115px;
padding: 0;
text-align: center;
background: no-repeat center url("/assets/images/background.webp");
background: no-repeat center image-set(url("/assets/images/background.webp") 1x, url("/assets/images/background.jpeg") 1x);
border-radius: 4px;
background-size: 1280px 200px;
width: 1280px;
height: 200px;
margin: auto;
}
article {
width: 1280px;
margin: auto auto;
@ -69,18 +82,19 @@ article {
}
* {
font-family: 'Noto Sans', sans-serif;
font-family: 'Cardo', sans-serif;
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6 {
font-family: Georgia, "Times New Roman", Times, serif;
font-family: 'Cardo', serif;
line-height: 1.2;
}
h1 {
font-size: 3.4rem;
margin-bottom: .8rem;
font-family: "Garamond Premier Pro Display", Sans-serif;
font-size: 39px;
font-weight: 600;
}
h2 {
@ -122,11 +136,11 @@ blockquote {
}
blockquote p {
font: italic 1.2rem/1.6 Georgia, "Times New Roman", Times, serif;
font: italic 1.2rem/1.6 'Cardo', serif;
}
local-businesses local-business p {
font-family: 'Noto Sans', sans-serif;
font-family: 'Cardo', sans-serif;
}
.error {
@ -162,44 +176,41 @@ ow-nav > ow-path[selected="selected"] > svg {
.btn {
cursor: pointer;
border-radius: 5px;
box-shadow: 0 10px 20px -6px rgba(0, 0, 0, .12);
position: relative;
margin-bottom: 20px;
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
user-select: none;
font-size: 1rem;
line-height: 1.5;
transition: color .15s ease-in-out,
background-color .15s ease-in-out,
border-color .15s ease-in-out,
box-shadow .15s ease-in-out;
width: auto;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
border: 1px solid #495057;
color: #495057;
background: white;
border: 1px solid black;
color: white;
background: black;
padding: 10px 20px;
font-family: "Cardo", Sans-serif;
font-size: 20px;
font-weight: 500;
line-height: 1em;
letter-spacing: 0;
transition: all 0.2s;
}
a.btn {
text-decoration: none;
line-height: 2;
}
privacy-policy h2 {
display: block;
text-align: center;
}
privacy-policy h4 {
display: block;
text-align: center;
margin-bottom: 8px;
}
privacy-policy ul li {
list-style: circle;
}

BIN
assets/fonts/Cardo-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -55,11 +55,13 @@ customElements.define('business-item-editor', class extends Component {
<article id="list">
<slot></slot>
</article>
<article id="form">
<register-item-form-row remove="hidden" save="show" idx="0">
<register-business-item-form remove="hidden" save="show" idx="0">
<div id="shadow" slot="head">&nbsp;</div>
</register-item-form-row>
</register-business-item-form>
</article>
<article id="new-business-item-form">
<form id="create-new-item-form" method="post" action="/business-item/new">
<input type="hidden" name="name" id="name" />
@ -68,6 +70,7 @@ customElements.define('business-item-editor', class extends Component {
<input type="hidden" name="item_order" id="item_order" />
</form>
</article>
<form id="moveForm" action="/business-item/move" method="post">
<input name="id" type="hidden" />
<input name="item_order" type="hidden" />

View File

@ -19,6 +19,9 @@ customElements.define('business-item', class extends Component {
#move svg {
cursor: pointer;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
@media(min-width: 1280px) {
section > form {
display: flex;
@ -28,9 +31,15 @@ customElements.define('business-item', class extends Component {
width: 25%;
max-width: 25%;
}
form > #fields {
width: 50%;
}
#move {
width: 33px;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
}
#move-up, #move-down {
border: none;
@ -52,14 +61,18 @@ customElements.define('business-item', class extends Component {
</button>
</div>
<image-input send-original="true"></image-input>
<div id="fields">
<div>
<label>Nazwa</label>
<input name="name" id="name" />
<input name="name" id="name" class="input" />
</div>
<div>
<label>Cena</label>
<price-input></price-input>
<price-input class="input"></price-input>
</div>
</div>
<div id="actions">
<input type="submit" id="save" value="Zapisz" />
<input type="button" id="delete" value="Usuń" />

View File

@ -8,9 +8,8 @@ customElements.define('contact-info-editor', class extends Component {
constructor() {
super(`
<style>
:host {
display: block;
}
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
#contact-wrapper contact-type-icon {
width: 24px;
margin-right: 8px;
@ -29,6 +28,10 @@ customElements.define('contact-info-editor', class extends Component {
:host([save="false"]) #submit {
display: none;
}
input[type=submit] {
width: 100%;
max-width: 100%;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
@ -37,7 +40,6 @@ customElements.define('contact-info-editor', class extends Component {
${ FORM_STYLE }
</style>
<article>
<h2>Edycja listy danych kontaktowych</h2>
<section>
<form method="post" action="/contacts/create">
<div id="contact-wrapper">

View File

@ -10,19 +10,14 @@ customElements.define('edit-contact-info', class extends Component {
<style>
:host { display: block; }
article {
display: flex;
justify-content: space-between;
width: 100%;
}
#actions {
display: flex;
justify-content: space-between;
display: block;
}
#buttons {
display: flex;
justify-content: start;
margin: 8px 0;
}
#actions > *:not(:last-child) {
#actions input {
margin-right: 8px;
}
:host([mode = 'view']) contact-info-editor {
@ -44,6 +39,17 @@ customElements.define('edit-contact-info', class extends Component {
:host([delete = "false"]) #deleteButton {
display: none;
}
@media only screen and (min-device-width: 1000px) {
article {
display: flex;
justify-content: space-between;
width: 100%;
}
#actions {
display: flex;
justify-content: space-between;
}
}
${ BUTTON_STYLE }
</style>
<article>

View File

@ -9,7 +9,7 @@ customElements.define('local-business-item', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
:host([picture-url = '']) #img {
display: none;
}

View File

@ -9,7 +9,7 @@ customElements.define('local-business', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
#items {
margin-top: 16px;
}
@ -36,14 +36,6 @@ customElements.define('local-business', class extends Component {
`);
}
connectedCallback() {
super.connectedCallback();
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
}
get name() {
return this.getAttribute('name') || ''
}

View File

@ -9,7 +9,7 @@ customElements.define('local-business-list', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
::slotted(local-business[local-business-visible="invisible"]) {
display: none;
}

View File

@ -5,12 +5,12 @@ customElements.define('login-form', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
${ FORM_STYLE }
</style>
<form action="/login" method="post">
<div>
<label>Login</label>
<label>E-Mail</label>
<input name="email" placeholder="E-Mail" type="email" required />
</div>
<div>

View File

@ -8,6 +8,7 @@ customElements.define('marketplace-offer', class extends Component {
}
constructor() {
// language=HTML
super(`
<style>
:host {
@ -16,23 +17,31 @@ customElements.define('marketplace-offer', class extends Component {
border-bottom: 1px solid var(--border-light-gray-color);
width: 100%;
}
section {
margin-bottom: 16px;
}
#preview {
width: 100%;
}
image-popup {
max-width: 100%;
}
#sep {
display: block;
grid-area: sep;
text-align: center;
}
:host([price-range-max="0"]) #sep {
:host([price-range-max="0"]) #sep, :host([price-range]) #sep,
:host([price-range-max="0"]) #price-min, :host([price-range]) #price-min
{
display: none;
}
#details {
display: grid;
column-gap: 16px;
@ -40,24 +49,39 @@ customElements.define('marketplace-offer', class extends Component {
"desc desc desc"
"min sep max";
grid-template-columns: auto 10px auto;
width: 90%;
}
#preview {
grid-area: img;
max-width: 200px;
margin: auto;
}
#description {
grid-area: desc;
text-align: justify;
font-style: italic;
}
#price-min {
grid-area: min;
justify-self: end;
-webkit-transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
-o-transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s, -webkit-box-shadow .3s;
}
#price-max {
grid-area: max;
justify-self: start;
-webkit-transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
-o-transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s, -webkit-box-shadow .3s;
}
#contacts {
display: flex;
justify-content: end;
@ -67,41 +91,42 @@ customElements.define('marketplace-offer', class extends Component {
#details {
display: grid;
column-gap: 16px;
grid-template-areas: "img desc desc desc desc desc"
"img _ _ min sep max";
grid-template-columns: 400px auto auto 100px 10px 100px;
grid-template-areas: "img img img" "desc desc desc" "min sep max";
grid-template-columns: auto 10px auto;
margin: 0 20px 0 20px;
}
image-popup {
width: 100%;
}
image-popup {
width: 400px;
height: 400px;
}
#preview {
max-width: 400px;
max-height: 400px;
grid-area: img;
align-self: center;
text-align: center;
}
#description {
min-height: 200px;
grid-area: desc;
justify-self: stretch;
margin-top: 0;
}
#price-min {
grid-area: min;
justify-self: end;
width: 100px;
text-align: right;
}
#price-max {
grid-area: max;
justify-self: start;
width: 100px;
text-align: left;
text-align: right;
}
}
${ INPUT_STYLE }
</style>
<section id="details">

View File

@ -1,6 +1,10 @@
import { Component } from "../shared";
import { Component, BUTTON_STYLE } from "../shared";
customElements.define('marketplace-offers', class extends Component {
static get observedAttributes() {
return ['account-id'];
}
constructor() {
super(`
<style>
@ -11,18 +15,48 @@ customElements.define('marketplace-offers', class extends Component {
display: block;
text-align: center;
}
article {
margin: 8px;
#offers {
display: block;
}
::slotted(marketplace-offer), ::slotted(user-edit-offer) {
width: 100%;
margin: 0 20px 20px;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
#offers {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
::slotted(marketplace-offer), ::slotted(user-edit-offer) {
width: calc(33% - 40px);
margin: 0 20px 20px;
}
}
${ BUTTON_STYLE }
</style>
<article>
<slot></slot>
<section>
<button id="publish" class="btn">Dodaj ogłoszenie</button>
</section>
<section><slot></slot></section>
<section id="offers">
<slot name="offer"></slot>
</section>
</article>
`);
this.shadowRoot.querySelector('#publish').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
location.href = '/account/offers';
});
}
set account_id(v) {
this.setAttribute('account-id', v)
}
get account_id() {
return this.getAttribute('account-id');
}
});

View File

@ -16,15 +16,11 @@ customElements.define('user-edit-offer', class extends Component {
:host([mode='form']) #form { display: block; }
:host([state='Finished']) #finishForm { display: none; }
#actions {
width: 120px;
display: flex;
justify-content: space-between;
}
#actions > * {
margin-right: 16px;
}
#actions input {
width: 100%;
width: 140px;
}
#state {
font-weight: bold;
@ -34,26 +30,19 @@ customElements.define('user-edit-offer', class extends Component {
}
@media only screen and (min-device-width: 1200px) {
:host([mode='view']) #view { display: flex; }
#actions {
margin-left: 16px;
display: block;
}
#actions > * {
margin-right: 0;
}
}
${ BUTTON_STYLE }
</style>
<section id="state"></section>
<section id="view">
<slot></slot>
<div id="actions">
<div id="state"></div>
</section>
<section id="actions">
<input type="button" value="Edytuj" id="edit" />
<form id="finishForm" action="/offers/finish" method="post">
<input type="hidden" name="id" class="id" value="" />
<input type="submit" slot="action" id="finish" value="Zakończ" />
</form>
</div>
</section>
<section id="form">
<offer-form>
@ -65,7 +54,7 @@ customElements.define('user-edit-offer', class extends Component {
this.shadowRoot.querySelector('#edit').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'form';
location.href = `/marketplace/${this.offer_id}/edit`
});
this.shadowRoot.querySelector('#cancel').addEventListener('click', ev => {
ev.preventDefault();

View File

@ -40,7 +40,11 @@ customElements.define('account-view', class extends Component {
}
#logout {
border-color: var(--red-color);
color: var(--red-color);
background: var(--red-color);
color: white;
}
#logoutSection {
margin-bottom: 16px;
}
section input[type="button"],
section input[type="button"],
@ -48,13 +52,10 @@ customElements.define('account-view', class extends Component {
width: 100%;
}
section input[type="submit"],
section a.btn {
padding-bottom: 0;
padding-top: 0;
display: block;
section a {
color: black;
}
::slotted(#editService) {
width: calc(100% - 1.5rem) !important;
padding-bottom: 0 !important;
padding-top: 0 !important;
display: block !important;
@ -77,11 +78,39 @@ customElements.define('account-view', class extends Component {
width: auto;
}
::slotted(#editService) {
min-width: 120px !important;
width: auto !important;
display: inline-block !important;
}
}
::slotted(button) {
cursor: pointer;
position: relative;
display: inline-block;
text-align: center;
vertical-align: middle;
user-select: none;
height: calc(1.5em + 0.75rem + 2px);
border: 1px solid black;
color: white;
background: black;
padding: 10px 20px;
font-family: "Cardo", Sans-serif;
font-size: 20px;
font-weight: 500;
line-height: 1em;
letter-spacing: 0;
transition: all 0.2s;
width: 100%;
}
#editServiceSection {
margin-bottom: 16px;
width: 100%;
}
label {
width: 120px;
}
.input {
margin-bottom: 16px;
}
${ FORM_STYLE }${ BUTTON_STYLE }
</style>
<article>
@ -89,11 +118,11 @@ customElements.define('account-view', class extends Component {
<section>
<input id="id" name="id" readonly type="hidden" />
</section>
<section>
<section class="input">
<label>Login</label>
<input id="name" name="name" readonly />
</section>
<section>
<section class="input">
<label>E-Mail</label>
<input id="email" name="email" readonly />
</section>
@ -106,17 +135,17 @@ customElements.define('account-view', class extends Component {
<label>Powiązane konto Facebook</label>
<input id="facebook_id" name="facebook_id" readonly />
</section>
<section>
<section id="editServiceSection">
<slot name="editService"></slot>
</section>
<section>
<section id="logoutSection">
<form action="/logout" method="post">
<input id="logout" value="Wyloguj" type="submit" />
</form>
</section>
<section id="rules">
<a class="btn" href="/terms-and-condition" target="_blank">Regulamin</a>
<a class="btn" href="/privacy-policy" target="_blank">Polityka prywatności</a>
<a class="" href="/terms-and-condition" target="_blank">Regulamin</a>
<a class="" href="/privacy-policy" target="_blank">Polityka prywatności</a>
</section>
</article>
`);
@ -124,6 +153,14 @@ customElements.define('account-view', class extends Component {
ev.preventDefault();
ev.stopPropagation();
});
{
const input = this.querySelector('#editService');
if (input) input.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
location.href = "/account/business-items";
})
}
}
connectedCallback() {

View File

@ -1,4 +1,4 @@
import { Component, FORM_STYLE, goTo, historyDetails } from "../shared";
import { Component, BUTTON_STYLE, Router } from "../shared";
customElements.define('ow-account', class extends Component {
static get observedAttributes() {
@ -9,7 +9,7 @@ customElements.define('ow-account', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
#switch-register, #switch-login {
display: none;
}
@ -19,38 +19,39 @@ customElements.define('ow-account', class extends Component {
:host([mode="register"]) #switch-login {
display: block !important;
}
a {
input {
display: block;
cursor: pointer;
}
a:hover {
color: var(--hover-color);
border-bottom-color: var(--hover-color);
text-decoration: none;
section > input[type=button] {
width: 100%;
max-width: 100%;
}
@media only screen and (min-device-width: 1000px) {
}
${ FORM_STYLE }
${ BUTTON_STYLE }
</style>
<article>
<slot></slot>
<section id="switch-register">
<a class="btn">Nie masz konta? Utwórz nowe</a>
<input type="button" class="btn" value="Nie masz konta? Utwórz nowe" />
</section>
<section id="switch-login">
<a class="btn">Posiadasz konto? Zaloguj się</a>
<input type="button" class="btn" value="Posiadasz konto? Zaloguj się" />
</section>
</article>
`);
this.shadowRoot.querySelector('#switch-login > a').addEventListener('click', ev => {
this.shadowRoot.querySelector('#switch-login > input').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
goTo('/login');
Router.goTo('/login');
});
this.shadowRoot.querySelector('#switch-register > a').addEventListener('click', ev => {
this.shadowRoot.querySelector('#switch-register > input').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
goTo('/register/account-type');
Router.goTo('/register/account-type');
});
this.addEventListener('facebook:account', ev => {
ev.stopPropagation();
@ -60,24 +61,24 @@ customElements.define('ow-account', class extends Component {
connectedCallback() {
super.connectedCallback();
const parts = historyDetails().parts;
this.listenHistory(Router.historyDetails());
}
listenHistory = ({ parts }) => {
switch (parts.first) {
case 'register': {
if (this.mode === 'register')
return;
this.mode = 'register';
break;
}
default:
default: {
if (this.mode === 'login')
return;
this.mode = 'login';
break;
}
}
listenHistory = ({ parts }) => {
console.warn(parts);
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
}
get mode() {
@ -87,5 +88,14 @@ customElements.define('ow-account', class extends Component {
set mode(value) {
value = ['login', 'register', 'display'].includes(value) ? value : 'login';
this.setAttribute('mode', value);
switch (value) {
case 'register': {
this.innerHTML = `<register-form></register-form>`;
break;
}
default:
this.innerHTML = `<login-form></login-form>`;
break;
}
}
});

View File

@ -1,4 +1,4 @@
import { FORM_STYLE, Component, goTo } from "./shared.js";
import { FORM_STYLE, Component, Router } from "./shared.js";
import { RegisterForm } from "./register-form/model.js";
customElements.define('register-form', class extends Component {
@ -12,7 +12,7 @@ customElements.define('register-form', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
.actions {
display: flex;
@ -73,15 +73,15 @@ customElements.define('register-form', class extends Component {
#loadCache() {
let register = {};
try {
register = JSON.parse(localStorage.getItem('register'));
register = JSON.parse(localStorage.getItem('register') || '{}');
} catch (e) {
localStorage.removeItem('register');
}
this.#form.from(register);
}
#copyDetail(ev) {
ev.stopPropagation();
console.info('ev.detail', ev.detail);
for (const [key, value] of Object.entries(ev.detail)) {
this.#form[key] = value;
}
@ -203,7 +203,7 @@ customElements.define('register-form', class extends Component {
? this.#nextPage()
: this.#prevPage();
this.current = current;
goTo(`/register/${ current }`);
Router.goTo(`/register/${ current }`);
}
get current() {

View File

@ -159,7 +159,8 @@ export class RegisterForm {
}, {})
}
from(object) {
from(object = {}) {
object = object || {};
[
'email',
'login',

View File

@ -21,8 +21,6 @@ customElements.define('register-account-type', class extends Component {
}
a {
display: block;
border: 1px solid var(--border-slim-color);
border-radius: 16px;
padding: 16px;
text-align: center;
cursor: pointer;
@ -57,21 +55,22 @@ customElements.define('register-account-type', class extends Component {
}
#rules > a {
display: block;
font-size: 20px;
}
}
${ BUTTON_STYLE }${ TIP_STYLE }
</style>
<article>
<section id="rules">
<a class="btn" target="_blank" href="/terms-and-condition">
<a target="_blank" href="/terms-and-condition">
Regulamin
</a>
<a class="btn" target="_blank" href="/privacy-policy">
<a target="_blank" href="/privacy-policy">
Polityka prywatności
</a>
</section>
<button id="accept-terms">
<h5>Zapoznałem się i zgadzam się</h5>
Zapoznałem się i zgadzam się
</button>
</article>
`);
@ -110,7 +109,7 @@ customElements.define('register-account-type', class extends Component {
</a>
</li>
</ul>
<form-navigation></form-navigation>
<form-navigation style="display: none;"></form-navigation>
`;
const user = this.shadowRoot.querySelector('#user');

View File

@ -10,15 +10,24 @@ customElements.define('register-business-contacts-form', class extends RegisterF
display: flex;
justify-content: space-between;
}
contact-info-editor > input[type='button'] {
width: 100%;
max-width: 100%;
}
::slotted(edit-contact-info) {
margin-bottom: 16px;
}
${ BUTTON_STYLE }
</style>
<article>
<h2>Edycja listy danych kontaktowych</h2>
<form>
<section>
<contact-info-editor
save="false"
></contact-info-editor>
>
<input type="button" id="addButton" value="Dodaj" />
</contact-info-editor>
</section>
<slot></slot>
<form-navigation></form-navigation>
@ -26,19 +35,16 @@ customElements.define('register-business-contacts-form', class extends RegisterF
</article>
`);
this.shadowRoot.querySelector('#addButton').addEventListener('click', ev => {
const editor = this.shadowRoot.querySelector('contact-info-editor');
editor.addEventListener('submit', ev => {
ev.stopPropagation();
ev.preventDefault();
const form = this.shadowRoot.querySelector('contact-info-editor');
const { type, content } = form;
this.innerHTML += `
<edit-contact-info mode="view" delete="false">
<contact-info
type="${ type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`;
this.#addContact(editor);
});
editor.querySelector('#addButton').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#addContact(editor);
});
this.mountFormHandler(() => this.#emitChange());
@ -52,6 +58,18 @@ customElements.define('register-business-contacts-form', class extends RegisterF
return Array.from(this.querySelectorAll('contact-info'));
}
#addContact(editor) {
const { type, content } = editor;
this.innerHTML += `
<edit-contact-info mode="view" delete="false">
<contact-info
type="${ type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`;
}
#emitChange() {
const rows = this.#rows;
const contacts = rows.map(({ type, content }) => ({

View File

@ -5,12 +5,17 @@ customElements.define('register-business-details-form', class extends RegisterFo
static get observedAttributes() {
return ["name", "description"];
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
textarea { min-height: 200px; }
form > div > input,
form > div > textarea {
width: 100%;
}
${ FORM_STYLE }${ TIP_STYLE }
</style>
<form id="step-2">

View File

@ -10,16 +10,30 @@ customElements.define('register-business-item-form', class extends RegisterFormC
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
section > form input[type=button], section > form input[type=submit] {
width: 100%;
max-width: 100%;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
@media only screen and (min-device-width: 1200px) {
section > form {
display: flex;
justify-content: space-between;
}
}
form > input[type=submit] {
section > form input[type=button], section > form input[type=submit] {
width: 25%;
max-width: 25%;
}
form > #fields {
width: 50%;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
}
img[src=""] { display: none; }
${ FORM_STYLE }
</style>
@ -27,6 +41,7 @@ customElements.define('register-business-item-form', class extends RegisterFormC
<form method="post">
<slot name="head"></slot>
<image-input send-original="true"></image-input>
<div id="fields">
<div id="name">
<label>Nazwa</label>
<input id="name" class="item-name" name="name" type="text" required />
@ -36,6 +51,7 @@ customElements.define('register-business-item-form', class extends RegisterFormC
<price-input id="price" class="item-price" name="price" required >
</price-input>
</div>
</div>
<input id="submit-button" type="submit" value="Zapisz" />
<input id="remove-button" type="submit" value="Usuń" />
@ -137,7 +153,6 @@ customElements.define('register-business-item-form', class extends RegisterFormC
set price(v) {
v = parseInt(v);
console.info('set price', v);
this.setAttribute('price', v);
this.shadowRoot.querySelector('#price').value = v / 100.0;
}

View File

@ -14,7 +14,7 @@ customElements.define('register-business-items-form', class extends RegisterForm
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
::slotted(section) {
display: flex;
@ -23,6 +23,10 @@ customElements.define('register-business-items-form', class extends RegisterForm
::slotted(register-business-item-form) {
margin-bottom: 18px;
}
form input[type=button], form input[type=submit] {
width: 100%;
max-width: 100%;
}
${ FORM_STYLE }
</style>
@ -68,10 +72,7 @@ customElements.define('register-business-items-form', class extends RegisterForm
}
#emitChange() {
const rows = this.#rows;
console.warn(rows);
const items = this.#rows.map(({ price, picture_url, name }) => ({ price, picture_url, name }));
console.warn(items);
this.dispatchEvent(new CustomEvent(this.submitEventName, { bubbles: true, composed: true, detail: { items } }));
}
});

View File

@ -1,4 +1,5 @@
import { FORM_STYLE, goTo, PseudoForm } from "../shared.js";
import { FORM_STYLE, Router, PseudoForm } from "../shared.js";
import { ErrorMessage } from "../shared/error-message.js";
customElements.define('register-business-submit-form', class extends PseudoForm {
static get observedAttributes() {
@ -9,10 +10,14 @@ customElements.define('register-business-submit-form', class extends PseudoForm
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
${ FORM_STYLE }
img[src=''] { display: none; }
@media only screen and (min-device-width: 1200px) {
input[type=submit] {
width: 100%;
max-width: 100%;
}
@media only screen and (min-device-width: 1000px) {
.item-view {
display: flex;
justify-content: space-between;
@ -75,10 +80,11 @@ customElements.define('register-business-submit-form', class extends PseudoForm
});
if (res.ok) {
goTo("/account?success");
// Router.goTo("/account?success");
location.href ='/account?success';
} else {
const { error } = await res.json();
document.querySelector('error-message').message = error;
ErrorMessage.errorMessage = error;
}
});
}

View File

@ -1,5 +1,6 @@
import { FORM_STYLE } from "../shared";
import { RegisterFormComponent } from "./model";
import { FORM_STYLE, Router } from "../shared.js";
import { RegisterFormComponent } from "./model.js";
import { ErrorMessage } from "../shared/error-message";
customElements.define('register-user-account-form', class extends RegisterFormComponent {
static get observedAttributes() {
@ -10,13 +11,7 @@ customElements.define('register-user-account-form', class extends RegisterFormCo
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
:host([mode="email"]) #form {
display: block;
}
:host([mode="email"]) #email-icon {
display: none;
}
* { font-family: 'Cardo', sans-serif; }
svg > path {
fill: var(--border-slim-color);
}
@ -99,6 +94,34 @@ customElements.define('register-user-account-form', class extends RegisterFormCo
form.querySelector('#account_type').value = 'User';
form.submit();
});
form.addEventListener('submit', async ev => {
ev.stopPropagation();
ev.preventDefault();
const json = Array.from(form.elements).filter(el => el.name && el.name.trim().length).reduce((memo, {
name,
value
}) => ({
...memo,
[name]: value.trim(),
}), {});
const res = await fetch('/register', {
method: 'POST',
body: JSON.stringify(json),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
if (res.ok) {
// Router.goTo("/account?success");
location.href ='/account?success';
} else {
const { error } = await res.json();
ErrorMessage.errorMessage = error;
}
});
}
get submitEventName() {
@ -106,21 +129,10 @@ customElements.define('register-user-account-form', class extends RegisterFormCo
}
connectedCallback() {
super.connectedCallback();
this.mode = 'email';
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'mode': {
if (newV !== 'email' && newV !== 'facebook' && newV !== '')
return;
this.mode = newV;
break;
}
}
}
get mode() {
return this.getAttribute('mode') || ''
}

View File

@ -1,53 +1,43 @@
export const BUTTON_STYLE = `
input[type="button"], input[type="submit"], button {
cursor: pointer;
border-radius: 5px;
box-shadow: 0 10px 20px -6px rgba(0,0,0,.12);
position: relative;
margin-bottom: 20px;
transition: .3s;
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
user-select: none;
font-size: 1rem;
line-height: 1.5;
transition: color .15s ease-in-out,
background-color .15s ease-in-out,
border-color .15s ease-in-out,
box-shadow .15s ease-in-out;
width: auto;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
border: 1px solid #495057;
color: #495057;
background: white;
border: 1px solid black;
color: white;
background: black;
padding: 10px 20px;
font-family: "Cardo", Sans-serif;
font-size: 20px;
font-weight: 500;
line-height: 1em;
letter-spacing: 0;
transition: all 0.2s;
}
.btn {
cursor: pointer;
border-radius: 5px;
box-shadow: 0 10px 20px -6px rgba(0, 0, 0, .12);
position: relative;
margin-bottom: 20px;
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
user-select: none;
font-size: 1rem;
line-height: 1.5;
transition: color .15s ease-in-out,
background-color .15s ease-in-out,
border-color .15s ease-in-out,
box-shadow .15s ease-in-out;
width: auto;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
border: 1px solid #495057;
color: #495057;
background: white;
border: 1px solid black;
color: white;
background: black;
padding: 10px 20px;
font-family: "Cardo", Sans-serif;
font-size: 20px;
font-weight: 500;
line-height: 1em;
letter-spacing: 0;
transition: all 0.2s;
}
a.btn {
text-decoration: none;
@ -66,34 +56,25 @@ input.link:hover {
export const INPUT_STYLE = `
input, textarea, select, option {
font-size: 16px;
box-sizing: border-box;
border: 1px solid var(--ast-border-color);
padding: 6px 6px 5px;
margin: 0 4px 0 0;
outline: 0;
line-height: 1;
border: none;
border-bottom-style: none;
border-bottom-width: medium;
border-bottom: 1px solid rgba(0,0,0,.1);
color: #666;
padding: .75em;
height: auto;
border-width: 1px;
border-style: solid;
border-color: var(--ast-border-color);
border-radius: 2px;
padding: 0;
height: 36px;
background: #fff;
color: rgba(0,0,0,.8);
font-size: 14px;
box-shadow: none !important;
display: block;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-clip: padding-box;
transition: border-color .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
background: #fafafa;
background-color: rgb(250, 250, 250);
box-shadow: none;
box-sizing: border-box;
transition: all .2s linear;
}
input[type="text"],
input[type="number"],
@ -170,8 +151,8 @@ export class Component extends HTMLElement {
}
{
const listener = this.listenHistory;
listener && listener(historyDetails());
this.#dropHistory = subscribeHistory(listener);
listener && listener(Router.historyDetails());
this.#dropHistory = Router.subscribeHistory(listener);
}
}
@ -180,7 +161,9 @@ export class Component extends HTMLElement {
}
#dropHistory;
listenHistory = null
listenHistory() {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV)
@ -289,22 +272,38 @@ export const runFbReady = (fn) => {
const fbQueue = [];
let fbReady = false;
const historyQueue = new Set;
export const goTo = (url) => {
export class Router {
static historyQueue = new Set;
static goTo(url) {
history.pushState({}, document.title, url);
for (const call of historyQueue) call();
document.dispatchEvent(new CustomEvent('history:push', { composed: true, bubbles: true, details: historyDetails() }));
Router.onChange();
}
export const historyDetails = () => {
const parts = location.pathname.split('/').filter(s => s && s.length)
return { parts }
static historyDetails() {
return {
parts: location.pathname.split('/').filter(s => s && s.length),
}
const subscribeHistory = (cb) => {
}
static subscribeHistory(cb) {
if (cb) {
const call = () => {
cb(historyDetails());
cb(Router.historyDetails());
}
historyQueue.add(call);
return () => historyQueue.delete(call);
Router.historyQueue.add(call);
return () => Router.historyQueue.delete(call);
}
}
static onChange() {
for (const call of Router.historyQueue) call();
document.dispatchEvent(new CustomEvent('history:push', {
composed: true,
bubbles: true,
details: Router.historyDetails()
}));
}
}
window.addEventListener('popstate', () => Router.onChange());

View File

@ -1,6 +1,6 @@
import { Component } from "../shared.js";
customElements.define('error-message', class extends Component {
export class ErrorMessage extends Component {
static get observedAttributes() {
return ['message'];
}
@ -8,7 +8,8 @@ customElements.define('error-message', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
:host { display: none; }
:host([message]) { display: block; }
div {
width: 1280px;
background: #ffe0e0;
@ -30,4 +31,12 @@ customElements.define('error-message', class extends Component {
this.setAttribute('message', m);
this.shadowRoot.querySelector('div').textContent = m;
}
})
static set errorMessage(v) {
const el = document.querySelector('error-message');
if (!el) return;
el.message = v;
}
}
customElements.define('error-message', ErrorMessage)

View File

@ -9,17 +9,25 @@ customElements.define('form-navigation', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
.actions {
display: flex;
justify-content: space-between;
}
form > .actions > input {
max-width: 200px;
margin: 16px 0;
}
form > .actions > input.hidden {
display: none !important;
}
form > .actions > input {
width: calc(50% - 16px);
max-width: 200px;
}
@media only screen and (min-device-width: 1000px) {
form > .actions > input {
width: auto;
max-width: 200px;
}
}
${ BUTTON_STYLE }
</style>
<form>

View File

@ -1,41 +1,109 @@
import { Component } from "../../shared";
import { Component, Router } from "../../shared.js";
customElements.define('ow-nav', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
section {
display: flex;
align-items: stretch;
align-content: stretch;
margin-bottom: 12px;
color: #2b2727;
font-family: Raleway, sans-serif;
font-size: 14px;
line-height: 32px;
article {
border-style: solid;
border-width: 0 0 1px 0;
border-color: #F2F2F2;
transition: background 0.3s, border 0.3s, border-radius 0.3s, box-shadow 0.3s;
margin-top: 0;
margin-bottom: 16px;
padding: 13px 10px 13px 10px;
}
section::after {
display: block;
content: ' ';
border: none;
padding: 10px 18px;
text-decoration: none;
color: #2b2727;
text-transform: uppercase;
align-self: stretch;
flex-shrink: 20;
flex-grow: 20;
section, section > div {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
position: relative;
}
svg {
height: 48px;
}
#logo {
display: none;
}
@media print {
:host { display: none; }
}
@media only screen and (min-device-width: 1000px) {
section > div {
width: 33%;
margin: 0 auto;
}
#logo {
display: block;
text-align: center;
}
#right {
justify-content: flex-end;
}
}
</style>
<article>
<section>
<slot></slot>
<div class="left">
<slot name="left"></slot>
</div>
<div id="logo">
<a href="/">
<svg viewBox="0 0 911 550" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="293" cy="335.2" rx="121" ry="170.61" fill="none" stroke="#555" style="opacity:.5" opacity=".5"/>
<ellipse cx="653.783" cy="113.849" rx="121.002" ry="170.613" fill="none" stroke="#555" transform="rotate(11.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="428.125" cy="166.014" rx="125.003" ry="176.254" fill="none" stroke="#555" transform="rotate(22.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="402.034" cy="23.889" rx="123.005" ry="173.437" fill="none" stroke="#555" transform="rotate(33.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="464.706" cy="-31.682" rx="124.999" ry="176.248" fill="none" stroke="#555" transform="rotate(45)" style="opacity:.5" opacity=".5"/>
<ellipse cx="540.521" cy="-306.337" rx="129.005" ry="181.898" fill="none" stroke="#555" transform="rotate(56.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="494.207" cy="-442.566" rx="124.003" ry="174.844" fill="none" stroke="#555" transform="rotate(67.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="239.113" cy="-150.236" rx="126.002" ry="177.663" fill="none" stroke="#555" transform="rotate(78.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="304.2" cy="-330" rx="128" ry="180.48" fill="none" stroke="#555" transform="rotate(90)" style="opacity:.5" opacity=".5"/>
<ellipse cx="143.181" cy="-572.932" rx="120.002" ry="169.203" fill="none" stroke="#555" transform="rotate(101.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="142" cy="-386.769" rx="123.003" ry="173.434" fill="none" stroke="#555" transform="rotate(112.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-91.151" cy="-617.377" rx="123.005" ry="173.437" fill="none" stroke="#555" transform="rotate(123.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-17.541" cy="-483.099" rx="124.999" ry="176.248" fill="none" stroke="#555" transform="rotate(135)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-89.814" cy="-445.118" rx="128.005" ry="180.488" fill="none" stroke="#555" transform="rotate(146.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-309.998" cy="-338.613" rx="125.003" ry="176.254" fill="none" stroke="#555" transform="rotate(157.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-514.833" cy="-456.414" rx="120.002" ry="169.203" fill="none" stroke="#555" transform="rotate(168.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-534" cy="-351.2" rx="126" ry="177.66" fill="none" stroke="#555" transform="scale(-1)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-434.371" cy="-246.176" rx="128.002" ry="180.483" fill="none" stroke="#555" transform="rotate(-168.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-710.531" cy="4.041" rx="126.003" ry="177.664" fill="none" stroke="#555" transform="rotate(-157.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-382.027" cy="-19.162" rx="130.005" ry="183.308" fill="none" stroke="#555" transform="rotate(-146.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-721.398" cy="240.271" rx="129.999" ry="183.298" fill="none" stroke="#555" transform="rotate(-135)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-304.791" cy="106.633" rx="123.005" ry="173.437" fill="none" stroke="#555" transform="rotate(-123.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-495.302" cy="413.961" rx="128.003" ry="180.485" fill="none" stroke="#555" transform="rotate(-112.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-265.515" cy="206.161" rx="125.002" ry="176.253" fill="none" stroke="#555" transform="rotate(-101.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-180.2" cy="605" rx="122" ry="172.02" fill="none" stroke="#555" transform="rotate(-90)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-137.223" cy="326.045" rx="121.002" ry="170.613" fill="none" stroke="#555" transform="rotate(-78.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-80.373" cy="352.613" rx="125.003" ry="176.254" fill="none" stroke="#555" transform="rotate(-67.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-50.619" cy="275.587" rx="126.005" ry="177.667" fill="none" stroke="#555" transform="rotate(-56.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-22.773" cy="406.017" rx="126.999" ry="179.068" fill="none" stroke="#555" transform="rotate(-45)" style="opacity:.5" opacity=".5"/>
<ellipse cx="97.486" cy="309.523" rx="121.005" ry="170.617" fill="none" stroke="#555" transform="rotate(-33.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="507.007" cy="435.361" rx="129.003" ry="181.895" fill="none" stroke="#555" transform="rotate(-22.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="261.461" cy="254.093" rx="130.002" ry="183.303" fill="none" stroke="#555" transform="rotate(-11.25)" style="opacity:.5" opacity=".5"/>
<text xml:space="preserve" style="font-style:normal;font-weight:400;font-size:200px;line-height:1.25;font-family:sans-serif;fill:#000;fill-opacity:1;stroke:none" x="36.404" y="311.43">
<tspan x="36.404" y="311.43" style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-family:'Cardo';">OS Wilno</tspan>
</text>
</svg>
</a>
</div>
<div id="right">
<slot name="right"></slot>
</div>
</section>
</article>
`);
}
#mount(selector, path) {
const el = this.querySelector(selector);
if (!el) return;
el.addEventListener('click', (ev) => {
ev.stopPropagation();
ev.preventDefault();
Router.goTo(path);
});
}
});

View File

@ -9,7 +9,7 @@ customElements.define('ow-path', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
a {
padding: 8px;
text-decoration: none;

View File

@ -9,7 +9,7 @@ customElements.define('price-input', class extends Component {
super(`
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
#price {
font-weight: bold;
}

View File

@ -11,7 +11,7 @@ customElements.define('price-view', class extends HTMLElement {
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
* { font-family: 'Cardo', sans-serif; }
#price {
font-weight: bold;
}

View File

@ -9,9 +9,9 @@
facebook-id="{{a.facebook_id.as_deref().unwrap_or_default()}}"
>
{% if h.is_above_user(account) -%}
<a id="editService" slot="editService" href="/account/business-items" title="Moje usługi" class="btn">
<button id="editService" slot="editService" title="Moje usługi" class="">
Edytuj usługi
</a>
</button>
{%- endif %}
</account-view>
{% when None %}

View File

@ -14,12 +14,12 @@
<main>
{% match error %}
{% when Some with (e) %}
<error-message message="{{e}"></error-message>
<error-message message="{{e}}"></error-message>
{% when None %}
<error-message></error-message>
{% endmatch %}
<article>
{% include "nav.html" %}
<article>
{% block content %}{% endblock %}
</article>
<privacy-policy-bar></privacy-policy-bar>

View File

@ -0,0 +1,23 @@
{% extends "../base.html" %}
{% block content %}
<marketplace-offers>
<h2>Edycja oferty</h2>
<offer-form
state="{{offer.state.as_str()}}"
offer-id="{{offer.id}}"
description="{{offer.description}}"
picture-url="{{offer.picture_url}}"
{% match offer.price_range %}
{% when PriceRange::Free %}
price-range-min="0"
price-range-max="0"
{% when PriceRange::Fixed with { value } %}
price-range-min="{{value}}"
price-range-max="0"
{% when PriceRange::Range with { min, max } %}
price-range-min="{{min}}"
price-range-max="{{max}}"
{% endmatch %}
></offer-form>
</marketplace-offers>
{% endblock %}

View File

@ -1,17 +1,17 @@
{% extends "../base.html" %}
{% block content %}
{% if account.is_some() %}
<section>
<a href="/account/offers" class="btn">Opublikuj</a>
</section>
{% endif %}
<marketplace-offers>
<marketplace-offers
{% match account %}
{% when Some with (a) %}
account-id="{{a.id}}"
{% when None %}
{% endmatch %}
>
<h2 style="text-align: center">Rzeczy wystawione na sprzedaż</h2>
{% for offer in my_offers %}
<user-edit-offer
slot="offer"
state="{{offer.state.as_str()}}"
offer-id="{{offer.id}}"
description="{{offer.description}}"
@ -60,6 +60,7 @@
{% for offer in offers %}
<marketplace-offer
slot="offer"
offer-id="{{offer.id}}"
description="{{offer.description}}"
picture-url="{{offer.picture_url}}"

View File

@ -1,23 +1,16 @@
<ow-nav>
<ow-path path="/" id="home" title="OS Wilno">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511 511" xml:space="preserve">
<path d="M127.5 143h121.675a31.386 31.386 0 0 0-1.175 8.5 7.5 7.5 0 0 0 7.5 7.5h128a7.5 7.5 0 0 0 7.5-7.5c0-17.369-14.131-31.5-31.5-31.5-1.387 0-2.789.108-4.222.327C346.299 110.022 333.268 104 319.5 104s-26.799 6.022-35.778 16.327A27.875 27.875 0 0 0 279.5 120a31.314 31.314 0 0 0-17.439 5.284c-2.147-11.751-7.917-22.248-16.119-30.284H327.5a7.5 7.5 0 0 0 7.5-7.5c0-26.191-21.309-47.5-47.5-47.5-11.859 0-22.976 4.337-31.73 12.298A31.495 31.495 0 0 0 251.5 52c-16.921 0-31.106 11.904-34.643 27.775a56.764 56.764 0 0 0-10.588-1.006c-14.429 0-27.94 5.38-38.486 15.237a37.778 37.778 0 0 0-5.822-.468C138.824 93.539 120 112.362 120 135.5a7.5 7.5 0 0 0 7.5 7.5zm124-76c1.475 0 3.04.218 4.926.687a7.505 7.505 0 0 0 7.31-2.184C270.008 58.73 278.447 55 287.5 55c15.34 0 28.232 10.683 31.626 25H232.42c3-7.605 10.422-13 19.08-13zm28 68c1.482 0 3.046.276 4.923.868a7.502 7.502 0 0 0 8.413-2.87C298.933 124.233 308.901 119 319.5 119s20.567 5.233 26.665 13.998a7.5 7.5 0 0 0 8.413 2.87c1.877-.592 3.441-.868 4.923-.868 6.399 0 11.959 3.662 14.695 9h-109.39c2.735-5.338 8.295-9 14.694-9zm-117.539-26.461c1.96 0 4.019.285 6.479.896a7.498 7.498 0 0 0 7.311-2.183c8.051-8.694 18.889-13.482 30.518-13.482 20.45 0 37.512 14.788 41.056 34.231H136.061c3.257-11.23 13.635-19.462 25.9-19.462z"/>
<path d="M510.905 262.363c.06-2.283.095-4.571.095-6.863 0-68.247-26.577-132.408-74.834-180.666C387.908 26.577 323.747 0 255.5 0S123.092 26.577 74.834 74.834C26.577 123.092 0 187.253 0 255.5s26.577 132.408 74.834 180.666C123.092 484.423 187.253 511 255.5 511s132.408-26.577 180.666-74.834c45.966-45.966 72.242-106.365 74.635-170.975a7.538 7.538 0 0 0 .198-1.691 7.486 7.486 0 0 0-.094-1.137zM85.441 85.441C130.865 40.016 191.26 15 255.5 15s124.635 25.016 170.059 70.441C470.984 130.865 496 191.26 496 255.5c0 .167-.006.333-.006.5H463v-8.5a7.5 7.5 0 0 0-15 0v8.5h-17v-25h.5a7.499 7.499 0 0 0 5.303-12.803l-32-32A7.497 7.497 0 0 0 399.5 184h-80c-1.989 0-3.897.79-5.303 2.197l-26.92 26.92-2.015-2.418A7.5 7.5 0 0 0 279.5 208H263v-32.5a7.5 7.5 0 0 0-15 0v.5h-17v-.5a7.5 7.5 0 0 0-15 0v.5h-65v-.5a7.5 7.5 0 0 0-15 0v.5h-17v-.5a7.5 7.5 0 0 0-15 0V224H17.046c6.8-52.302 30.482-100.646 68.395-138.559zM494.769 280H431v-9h64.494c-.19 3.01-.425 6.012-.725 9zm-15.521 64H431v-49h61.776a238.363 238.363 0 0 1-13.528 49zm-15.521 32.011c-.076-.002-.15-.011-.227-.011h-416c-.076 0-.151.009-.227.011A238.142 238.142 0 0 1 38.279 359h434.44a237.097 237.097 0 0 1-8.992 17.011zm-15.951 24.003c-.092-.003-.183-.014-.276-.014h-384c-.093 0-.184.01-.276.014A239.546 239.546 0 0 1 56.743 391h397.515a240.711 240.711 0 0 1-6.482 9.014zM15 255.5c0-5.532.199-11.032.567-16.5H104v17H15.5c-.167 0-.33.014-.494.025 0-.175-.006-.35-.006-.525zM143.5 199a7.5 7.5 0 0 0 7.5-7.5v-.5h65v.5a7.5 7.5 0 0 0 15 0v-.5h17v17H119v-17h17v.5a7.5 7.5 0 0 0 7.5 7.5zM416 231v113h-97v-73.027c.168.011.335.027.504.027a7.5 7.5 0 0 0 5.757-12.301L302.179 231H416zm-110.394-15 17-17h73.787l17 17H305.606zM207 344v-73h97v73h-97zm-88 0V223h96.487l-29.749 35.699a7.5 7.5 0 0 0 .96 10.563 7.464 7.464 0 0 0 5.301 1.715V344H119zm184.487-88h-95.975l27.5-33h40.975l27.5 33zM104 271v73H31.752c-9.135-23.111-14.646-47.678-16.247-73H104zm151.5 225c-64.24 0-124.635-25.016-170.059-70.441A244.667 244.667 0 0 1 75.51 415h359.98a245.917 245.917 0 0 1-9.931 10.559C380.135 470.984 319.74 496 255.5 496z"/>
<path d="M143.5 240a7.5 7.5 0 0 0-7.5 7.5v16a7.5 7.5 0 0 0 15 0v-16a7.5 7.5 0 0 0-7.5-7.5zM167.5 240a7.5 7.5 0 0 0-7.5 7.5v16a7.5 7.5 0 0 0 15 0v-16a7.5 7.5 0 0 0-7.5-7.5zM143.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM167.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM55.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM39 319.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 15 0zM79.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM231.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM255.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM279.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM343.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM367.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM391.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM447.5 304a7.5 7.5 0 0 0-7.5 7.5v8a7.5 7.5 0 0 0 15 0v-8a7.5 7.5 0 0 0-7.5-7.5zM471.5 304a7.5 7.5 0 0 0-7.5 7.5v8a7.5 7.5 0 0 0 15 0v-8a7.5 7.5 0 0 0-7.5-7.5zM343.5 271a7.5 7.5 0 0 0 7.5-7.5v-8a7.5 7.5 0 0 0-15 0v8a7.5 7.5 0 0 0 7.5 7.5zM367.5 271a7.5 7.5 0 0 0 7.5-7.5v-8a7.5 7.5 0 0 0-15 0v8a7.5 7.5 0 0 0 7.5 7.5zM391.5 271a7.5 7.5 0 0 0 7.5-7.5v-8a7.5 7.5 0 0 0-15 0v8a7.5 7.5 0 0 0 7.5 7.5z"/>
</svg>
<span>OS Wilno</span>
</ow-path>
{# <ow-path path="/zero-waste" selected="{{ page.select_marketplace() }}" title="Za darmo">
{#
<ow-path path="/zero-waste" selected="{{ page.select_marketplace() }}" title="Za darmo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xml:space="preserve">
<path d="M21.787 265.97c0-93.686 60.433-175.027 147.281-203.282l-24.251 32.157c-3.623 4.804-2.666 11.634 2.137 15.256a10.84 10.84 0 0 0 6.551 2.197c3.302 0 6.565-1.496 8.706-4.335l42.916-56.904a10.893 10.893 0 0 0-5.155-16.861l-67.399-23.176c-5.69-1.954-11.888 1.07-13.844 6.759-1.955 5.69 1.07 11.888 6.759 13.844l33.485 11.515c-39.951 13.768-75.875 38.241-103.475 70.935C19.71 156.465 0 210.409 0 265.97c0 27.261 4.631 54.007 13.765 79.493 1.595 4.45 5.786 7.221 10.255 7.221 1.22 0 2.461-.207 3.674-.642 5.664-2.029 8.609-8.266 6.58-13.93-8.285-23.121-12.487-47.393-12.487-72.142zM414.025 402.991c-4.588-3.893-11.461-3.332-15.356 1.255-40.749 48.005-100.185 75.538-163.068 75.538-53.27 0-104.061-19.763-143.119-54.987l40.061 4.936c5.965.74 11.408-3.508 12.143-9.48.735-5.971-3.509-11.408-9.48-12.143l-70.739-8.715a10.896 10.896 0 0 0-12.025 12.895l13.628 69.958c1.013 5.202 5.572 8.813 10.681 8.813.691 0 1.391-.066 2.095-.204 5.905-1.15 9.76-6.871 8.609-12.776l-6.743-34.615c42.647 37.25 97.454 58.105 154.886 58.105 69.292 0 134.783-30.334 179.679-83.226 3.898-4.587 3.334-11.462-1.252-15.354zM509.325 288.789c-3.949-4.538-10.829-5.017-15.369-1.069l-27.12 23.593a236.333 236.333 0 0 0 4.366-45.343c0-55.56-19.71-109.503-55.497-151.896-35.361-41.885-84.382-70.28-138.033-79.955-5.917-1.072-11.586 2.866-12.653 8.788-1.068 5.921 2.866 11.586 8.787 12.654C375.561 73.91 449.415 162.4 449.415 265.969c0 14.664-1.487 29.157-4.413 43.346l-15.46-36.465c-2.349-5.539-8.745-8.124-14.281-5.777-5.539 2.349-8.126 8.742-5.778 14.281l27.821 65.619a10.89 10.89 0 0 0 17.179 3.966l53.772-46.781c4.54-3.95 5.018-10.83 1.07-15.369z"/>
<path d="M188.028 130.89a50.918 50.918 0 0 0-4.574-.207c-21.752 0-40.793 14.02-47.564 34.164-17.981 7.019-30.042 24.311-30.042 44.123a47.444 47.444 0 0 0 11.151 30.53 67.61 67.61 0 0 0-1.206 12.72c0 36.454 29.03 66.112 64.714 66.112 6.157 0 12.253-.886 18.119-2.63.11-.033.222-.059.333-.092a529.09 529.09 0 0 1-.877 107.238 10.891 10.891 0 0 0 10.829 12.088h53.379a10.896 10.896 0 0 0 10.828-12.088 529.532 529.532 0 0 1-1.495-100.106c17.407-.964 33.696-8.121 46.232-20.417 11.715-11.489 19.022-26.339 21-42.412 15.913-7.877 26.498-24.415 26.498-42.538 0-23.342-16.958-42.798-39.2-46.695-7.81-27.348-31.586-46.41-59.334-46.41a57.88 57.88 0 0 0-4.029.141c-9.275-8.797-21.549-13.741-34.507-13.741-16.076.004-30.876 7.585-40.255 20.22zm32.91 282.263a550.944 550.944 0 0 0-.314-99.862 68.285 68.285 0 0 1 20.675 4.615 71.561 71.561 0 0 0 8.631 2.762 551.612 551.612 0 0 0 .333 92.485h-29.325zm-17.889-265.307c4.906-9.489 14.577-15.385 25.237-15.385 8.511 0 16.501 3.771 21.924 10.348a10.89 10.89 0 0 0 10.256 3.805 37.464 37.464 0 0 1 6.354-.551c19.726 0 36.41 15.343 39.668 36.483.83 5.388 5.53 9.307 10.966 9.232l.493-.016c14.127 0 25.62 11.493 25.62 25.62 0 11.212-7.49 21.298-18.217 24.529a10.896 10.896 0 0 0-7.749 10.2c-.573 27.004-23.015 48.973-50.026 48.973-6.323 0-12.48-1.161-18.3-3.45-10.586-4.165-21.761-6.264-32.818-6.264-8.172 0-16.283 1.147-24.042 3.455a41.728 41.728 0 0 1-11.907 1.726c-23.671 0-42.927-19.884-42.927-44.325 0-4.38.624-8.722 1.855-12.905a10.895 10.895 0 0 0-3.206-11.212 25.651 25.651 0 0 1-8.595-19.135c0-11.949 8.114-22.2 19.73-24.93a10.893 10.893 0 0 0 8.207-8.556c2.555-13.336 14.279-23.015 27.881-23.015 2.429 0 4.843.307 7.174.914a10.9 10.9 0 0 0 12.422-5.541z"/>
<path d="M264.285 208.492c9.069 6.257 14.271 15.333 14.271 24.897 0 6.017 4.878 10.894 10.894 10.894 6.015 0 10.894-4.877 10.894-10.894 0-16.834-8.632-32.445-23.683-42.83-4.953-3.416-11.736-2.171-15.153 2.78-3.419 4.951-2.174 11.736 2.777 15.153zM234.981 191.995l.338.001h.085c5.977 0 10.846-4.823 10.891-10.811.046-6.017-4.794-10.931-10.81-10.976l-.503-.002c-6.017 0-10.894 4.877-10.894 10.894.001 6.016 4.877 10.894 10.893 10.894z"/>
</svg>
</ow-path> #}
</ow-path>
#}
{% if page.is_public() -%}
<ow-path path="/marketplace" selected="{{ page.select_marketplace() }}" title="Targ">
<ow-path path="/marketplace" selected="{{ page.select_marketplace() }}" title="Targ" slot="left">
<svg viewBox="0 0 484.909 484.909" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path d="M204.993 438.478c-6.347 6.349-6.347 16.639 0 22.978a16.196 16.196 0 0 0 11.488 4.761c4.158 0 8.316-1.587 11.489-4.761l49.747-49.754-22.979-22.978zm112.649-112.671-16.947 16.954 22.976 22.977 39.926-39.931zm-56.872 0h-45.954l135.627 135.648a16.193 16.193 0 0 0 11.487 4.761c4.158 0 8.315-1.587 11.488-4.761 6.349-6.339 6.349-16.629 0-22.978zM102.294 107.658c21.471 0 38.878-19.915 38.878-44.478 0-24.564-17.407-44.487-38.878-44.487-21.486 0-38.877 19.923-38.877 44.487 0 24.563 17.391 44.478 38.877 44.478zm-15.17 48.128c-58.083-103.857-29.041-51.929 0 0z"/>
<path d="M74.524 123.66c-7.062.128-11.934.302-12.44.539-5.554 1.365-19.132 13.9-21.512 19.605L1.42 250.377c-3.937 9.521.586 20.439 10.107 24.382a18.79 18.79 0 0 0 7.14 1.42c7.315 0 14.266-4.34 17.249-11.537l1.635-3.966c18.146-117.982 15.439 106.05 15.472 183.143 0 12.369 10.028 22.398 22.389 22.398 12.361 0 22.39-10.029 22.39-22.398V331.622h8.982v112.196c0 12.369 10.029 22.398 22.39 22.398s22.39-10.029 22.39-22.398c-.011-79.908-26.343-323.038 35.094-186.958 1.38 3.056 4.269 8.803 5.911 10.186.265.222 3.555 4.423 10.718 5.197.816.088 6.57-1.904 8.384-3.461 2.978-2.56 7.84-16.93 1.731-31.307-6.108-14.377-35.47-78.46-42.953-95.453-7.483-16.992-22.598-16.71-25.832-17.128-5.814-.751-11.658-.702-12.642-.736-13.92-.48-18.043.394-57.452-.498z"/>
@ -26,14 +19,14 @@
</svg>
<div>Targ</div>
</ow-path>
<ow-path path="/" selected="{{ page.select_index() }}" title="Lokalne usługi">
<ow-path path="/local-businesses" selected="{{ page.select_index() }}" title="Lokalne usługi" slot="left">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" xml:space="preserve">
<path d="M440.482 50.916c-8.099-4.226-18.091-1.088-22.321 7.013l-41.925 80.336-46.147 36.662h-54.683l-57.033-18.756-33.145-53.595 5.854-5.694c3.554-3.457 3.634-9.143.176-12.697l-13.735-14.12 10.294-10.014c4.024-3.914 4.113-10.35.198-14.374l-41.438-42.6c-3.914-4.024-10.35-4.113-14.374-.198l-66.5 64.687c-4.024 3.914-4.113 10.35-.198 14.374l41.438 42.599c3.914 4.024 10.35 4.113 14.374.198l10.294-10.013 13.737 14.121c3.449 3.547 9.136 3.64 12.696.176l2.896-2.817 32.466 52.499a16.544 16.544 0 0 0 8.901 7.014l58.436 19.218.008 287.213c0 10.964 8.888 19.853 19.852 19.853s19.852-8.888 19.852-19.853V333.466l8.565-2.239 24.462 64.136-17.398 78.459c-2.373 10.704 4.38 21.306 15.084 23.679 10.69 2.373 21.303-4.371 23.679-15.083l18.771-84.647a19.858 19.858 0 0 0-1.096-12.026l-24.153-57.154V202.66l51.197-40.674a16.547 16.547 0 0 0 4.375-5.299l43.551-83.45c4.23-8.1 1.089-18.094-7.01-22.321zM171.921 90.467a16.48 16.48 0 0 0-12.357 2.058c-6.031 3.73-8.81 10.704-7.514 17.272l-.092.088-7.475-7.683 20.167-19.617 7.475 7.684-.204.198z"/>
<circle cx="304.734" cy="129.707" r="34.286"/>
</svg>
<div>Lokalne Usługi</div>
</ow-path>
<ow-path path="/news" selected="{{ page.select_news() }}" title="Aktualności">
<ow-path path="/news" selected="{{ page.select_news() }}" title="Aktualności" slot="left">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 508 508" xml:space="preserve">
<path d="M437.4 197.6H307.6c-7.8 0-14.2 6.3-14.2 14.1v141.1c0 7.8 6.3 14.1 14.1 14.1h129.9c7.8 0 14.1-6.3 14.1-14.1V211.7c0-7.8-6.3-14.1-14.1-14.1zm-14 141.1H321.7V225.8h101.7v112.9zM360.7 409.3H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.7 6.3 14.1 14.1 14.1h290.1c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 338.7H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 268.1H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 197.6H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1z"/>
<path d="M493.9 0H14.1C6.3 0 0 6.3 0 14.1v479.8c0 7.8 6.3 14.1 14.1 14.1h403c3.7 0 7.3-1.5 10-4.1l76.8-76.8c2.6-2.6 4.1-6.2 4.1-10v-403C508 6.3 501.7 0 493.9 0zm-62.7 459.8v-28.6h28.6l-28.6 28.6zm48.6-56.8h-62.7c-7.8 0-14.1 6.3-14.1 14.1v62.7H28.2V28.2h451.6V403z"/>
@ -41,7 +34,7 @@
</svg>
<div>Aktualności</div>
</ow-path>
<ow-path path="/account" selected="{{ page.select_account() }}" title="Konto">
<ow-path path="/account" selected="{{ page.select_account() }}" title="Konto" slot="right">
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M16 7.992C16 3.58 12.416 0 8 0S0 3.58 0 7.992c0 2.43 1.104 4.62 2.832 6.09.016.016.032.016.032.032.144.112.288.224.448.336.08.048.144.111.224.175A7.98 7.98 0 0 0 8.016 16a7.98 7.98 0 0 0 4.48-1.375c.08-.048.144-.111.224-.16.144-.111.304-.223.448-.335.016-.016.032-.016.032-.032 1.696-1.487 2.8-3.676 2.8-6.106zm-8 7.001c-1.504 0-2.88-.48-4.016-1.279.016-.128.048-.255.08-.383a4.17 4.17 0 0 1 .416-.991c.176-.304.384-.576.64-.816.24-.24.528-.463.816-.639.304-.176.624-.304.976-.4A4.15 4.15 0 0 1 8 10.342a4.185 4.185 0 0 1 2.928 1.166c.368.368.656.8.864 1.295.112.288.192.592.24.911A7.03 7.03 0 0 1 8 14.993zm-2.448-7.4a2.49 2.49 0 0 1-.208-1.024c0-.351.064-.703.208-1.023.144-.32.336-.607.576-.847.24-.24.528-.431.848-.575.32-.144.672-.208 1.024-.208.368 0 .704.064 1.024.208.32.144.608.336.848.575.24.24.432.528.576.847.144.32.208.672.208 1.023 0 .368-.064.704-.208 1.023a2.84 2.84 0 0 1-.576.848 2.84 2.84 0 0 1-.848.575 2.715 2.715 0 0 1-2.064 0 2.84 2.84 0 0 1-.848-.575 2.526 2.526 0 0 1-.56-.848zm7.424 5.306c0-.032-.016-.048-.016-.08a5.22 5.22 0 0 0-.688-1.406 4.883 4.883 0 0 0-1.088-1.135 5.207 5.207 0 0 0-1.04-.608 2.82 2.82 0 0 0 .464-.383 4.2 4.2 0 0 0 .624-.784 3.624 3.624 0 0 0 .528-1.934 3.71 3.71 0 0 0-.288-1.47 3.799 3.799 0 0 0-.816-1.199 3.845 3.845 0 0 0-1.2-.8 3.72 3.72 0 0 0-1.472-.287 3.72 3.72 0 0 0-1.472.288 3.631 3.631 0 0 0-1.2.815 3.84 3.84 0 0 0-.8 1.199 3.71 3.71 0 0 0-.288 1.47c0 .352.048.688.144 1.007.096.336.224.64.4.927.16.288.384.544.624.784.144.144.304.271.48.383a5.12 5.12 0 0 0-1.04.624c-.416.32-.784.703-1.088 1.119a4.999 4.999 0 0 0-.688 1.406c-.016.032-.016.064-.016.08C1.776 11.636.992 9.91.992 7.992.992 4.14 4.144.991 8 .991s7.008 3.149 7.008 7.001a6.96 6.96 0 0 1-2.032 4.907z"/>
</svg>
@ -49,7 +42,7 @@
</ow-path>
{% if h.is_admin(account) -%}
<ow-path path="/admin" selected="{{ page.select_admin_news() }}" title="Admin">
<ow-path path="/admin" selected="{{ page.select_admin_news() }}" title="Admin" slot="right">
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<path d="M14.68 14.81a6.76 6.76 0 1 1 6.76-6.75 6.77 6.77 0 0 1-6.76 6.75Zm0-11.51a4.76 4.76 0 1 0 4.76 4.76 4.76 4.76 0 0 0-4.76-4.76Z"/>
<path d="M16.42 31.68A2.14 2.14 0 0 1 15.8 30H4v-5.78a14.81 14.81 0 0 1 11.09-4.68h.72a2.2 2.2 0 0 1 .62-1.85l.12-.11c-.47 0-1-.06-1.46-.06A16.47 16.47 0 0 0 2.2 23.26a1 1 0 0 0-.2.6V30a2 2 0 0 0 2 2h12.7Z"/>
@ -62,7 +55,7 @@
{%- endif %}
{% else if page.is_admin() -%}
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}" title="Zarządzaj aktualnościami">
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}" title="Zarządzaj aktualnościami" slot="left">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 508 508" xml:space="preserve">
<path d="M437.4 197.6H307.6c-7.8 0-14.2 6.3-14.2 14.1v141.1c0 7.8 6.3 14.1 14.1 14.1h129.9c7.8 0 14.1-6.3 14.1-14.1V211.7c0-7.8-6.3-14.1-14.1-14.1zm-14 141.1H321.7V225.8h101.7v112.9zM360.7 409.3H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.7 6.3 14.1 14.1 14.1h290.1c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 338.7H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 268.1H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 197.6H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1z"/>
<path d="M493.9 0H14.1C6.3 0 0 6.3 0 14.1v479.8c0 7.8 6.3 14.1 14.1 14.1h403c3.7 0 7.3-1.5 10-4.1l76.8-76.8c2.6-2.6 4.1-6.2 4.1-10v-403C508 6.3 501.7 0 493.9 0zm-62.7 459.8v-28.6h28.6l-28.6 28.6zm48.6-56.8h-62.7c-7.8 0-14.1 6.3-14.1 14.1v62.7H28.2V28.2h451.6V403z"/>
@ -71,8 +64,11 @@
<div>Aktualności</div>
</ow-path>
<ow-path path="/admin/businesses" selected="{{ page.select_admin_businesses() }}"
title="Zarządzaj usługami">
<ow-path
path="/admin/businesses" selected="{{ page.select_admin_businesses() }}"
title="Zarządzaj usługami"
slot="left"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" xml:space="preserve">
<path d="M440.482 50.916c-8.099-4.226-18.091-1.088-22.321 7.013l-41.925 80.336-46.147 36.662h-54.683l-57.033-18.756-33.145-53.595 5.854-5.694c3.554-3.457 3.634-9.143.176-12.697l-13.735-14.12 10.294-10.014c4.024-3.914 4.113-10.35.198-14.374l-41.438-42.6c-3.914-4.024-10.35-4.113-14.374-.198l-66.5 64.687c-4.024 3.914-4.113 10.35-.198 14.374l41.438 42.599c3.914 4.024 10.35 4.113 14.374.198l10.294-10.013 13.737 14.121c3.449 3.547 9.136 3.64 12.696.176l2.896-2.817 32.466 52.499a16.544 16.544 0 0 0 8.901 7.014l58.436 19.218.008 287.213c0 10.964 8.888 19.853 19.852 19.853s19.852-8.888 19.852-19.853V333.466l8.565-2.239 24.462 64.136-17.398 78.459c-2.373 10.704 4.38 21.306 15.084 23.679 10.69 2.373 21.303-4.371 23.679-15.083l18.771-84.647a19.858 19.858 0 0 0-1.096-12.026l-24.153-57.154V202.66l51.197-40.674a16.547 16.547 0 0 0 4.375-5.299l43.551-83.45c4.23-8.1 1.089-18.094-7.01-22.321zM171.921 90.467a16.48 16.48 0 0 0-12.357 2.058c-6.031 3.73-8.81 10.704-7.514 17.272l-.092.088-7.475-7.683 20.167-19.617 7.475 7.684-.204.198z"/>
<circle cx="304.734" cy="129.707" r="34.286"/>
@ -80,8 +76,11 @@
<div>Lokalne Usługi</div>
</ow-path>
<ow-path path="/admin/offers" selected="{{ page.select_admin_offers() }}"
title="Zarządzaj ofertami">
<ow-path
path="/admin/offers" selected="{{ page.select_admin_offers() }}"
title="Zarządzaj ofertami"
slot="left"
>
<svg viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path
d="M30.412 19.045a.34.34 0 0 0-.34.341V30.83c0 .12-.102.222-.222.222h-2.627v-3.14h.87a.402.402 0 0 0 .372-.22.402.402 0 0 0-.054-.43l-1.818-2.346a.436.436 0 0 0-.346-.173h-.003a.437.437 0 0 0-.347.179l-1.758 2.343a.403.403 0 0 0-.049.429.403.403 0 0 0 .372.218h.806v3.14h-8.941a.594.594 0 0 1-.594-.594V19.716c0-.327.266-.594.594-.594H27.7a.34.34 0 1 0 0-.681H16.327c-.703 0-1.275.572-1.275 1.275v10.742c0 .703.572 1.275 1.275 1.275h9.282a.34.34 0 0 0 .34-.34V27.57a.34.34 0 0 0-.34-.34h-.592l1.233-1.645 1.274 1.645h-.642a.34.34 0 0 0-.34.34v3.821a.34.34 0 0 0 .34.34h2.968a.905.905 0 0 0 .903-.903V19.386a.34.34 0 0 0-.34-.34z"

View File

@ -182,11 +182,17 @@ pub struct UpdateLocalBusinessInput {
pub description: String,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)]
pub enum PriceRange {
#[default]
Free,
Fixed { value: i32 },
Range { min: i32, max: i32 },
Fixed {
value: i32,
},
Range {
min: i32,
max: i32,
},
}
impl From<(i32, i32)> for PriceRange {
@ -286,7 +292,7 @@ pub struct CreateLocalBusinessItemInput {
pub name: String,
pub price: i64,
pub item_order: i32,
pub picture_url: String,
pub picture_url: Option<String>,
}
#[derive(Debug)]

View File

@ -97,7 +97,7 @@ impl Page {
pub struct BusinessItemInput {
pub name: String,
pub price: u32,
pub picture_url: String,
pub picture_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -250,6 +250,21 @@ pub struct Offer {
pub contacts: Vec<Arc<db::ContactInfo>>,
}
impl Default for Offer {
fn default() -> Self {
Self {
id: 0,
owner_id: 0,
price_range: Default::default(),
description: "".to_string(),
picture_url: "".to_string(),
state: Default::default(),
created_at: chrono::Utc::now().naive_utc(),
contacts: vec![],
}
}
}
impl From<(db::Offer, &Vec<Arc<db::ContactInfo>>)> for Offer {
fn from((offer, contacts): (db::Offer, &Vec<Arc<db::ContactInfo>>)) -> Self {
let db::Offer {

View File

@ -77,3 +77,14 @@ RETURNING id, login, email, pass, facebook_id, account_type
}
})
}
/* async fn delete_all_accounts() {
let _ = sqlx::query_as::<sqlx::Postgres, (i32,)>(
r#"
delete from contacts;
delete from local_business_items;
delete from local_businesses;
delete from accounts;
"#,
);
}*/

View File

@ -300,11 +300,13 @@ RETURNING
.bind(input.local_business_id)
.bind(&input.name)
.bind(input.price)
.bind(if input.picture_url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
input.picture_url.clone()
})
.bind(
input
.picture_url
.as_deref()
.map(String::from)
.unwrap_or_else(|| format!("--{}", uuid::Uuid::new_v4())),
)
.bind(input.item_order)
.fetch_one(t)
.await

View File

@ -104,6 +104,9 @@ pub enum Error {
offer_id: i32,
state: db::OfferState,
},
Offer {
offer_id: i32,
},
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@ -56,6 +56,34 @@ ORDER BY id DESC
})
}
#[tracing::instrument]
pub async fn offer_by_id(t: &mut T<'_>, account_id: i32, offer_id: i32) -> Result<db::Offer> {
sqlx::query_as(
r#"
SELECT
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
FROM offers
WHERE owner_id = $1 AND id = $2
LIMIT 1
"#,
)
.bind(account_id)
.bind(offer_id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::Offer { offer_id }
})
}
#[tracing::instrument]
pub async fn account_offers(t: &mut T<'_>, account_id: i32) -> Result<Vec<db::Offer>> {
sqlx::query_as(

View File

@ -159,7 +159,7 @@ async fn new_business_item(
name: form.name,
price: form.price,
item_order: form.item_order,
picture_url: form.picture_url,
picture_url: Some(form.picture_url),
},
)
.await

View File

@ -12,6 +12,7 @@ use serde::Serialize;
use crate::model::db;
use crate::model::view::{self, Page};
use crate::routes::HttpResult;
use crate::view::Helper;
#[derive(Default, Debug, Serialize, Template)]
@ -36,6 +37,11 @@ pub async fn render_index() -> HttpResponse {
)
}
#[get("/")]
async fn index(req: HttpRequest) -> HttpResult {
HttpResult::goto(&req, "/marketplace")
}
pub fn configure(config: &mut ServiceConfig) {
std::fs::create_dir_all("./uploads").expect("Failed to create ./uploads directory");
@ -43,15 +49,18 @@ pub fn configure(config: &mut ServiceConfig) {
account::configure(config);
news::configure(config);
marketplace::configure(config);
businesses::configure(config);
config
.service(Files::new("/uploads", "./uploads"))
.service(Files::new("/assets/images", "./assets/images"))
.service(Files::new("/assets/fonts", "./assets/fonts"))
.service(Files::new("/assets/css", "./client/dist"))
.service(
Files::new("/assets/js", "./client/dist")
.use_etag(true)
.prefer_utf8(true)
.show_files_listing(),
);
businesses::configure(config);
)
.service(index);
}

View File

@ -1,5 +1,3 @@
use std::collections::HashMap;
use actix_http::StatusCode;
use actix_web::web::{Data, Path, ServiceConfig};
use actix_web::{get, post, web, HttpRequest, HttpResponse};
@ -237,6 +235,20 @@ impl RegisterPage {
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
struct RegisterForm {
email: String,
login: String,
password: String,
facebook_id: Option<String>,
account_type: AccountType,
items: Option<Vec<view::BusinessItemInput>>,
contacts: Option<Vec<view::CreateContactInfoInput>>,
name: Option<String>,
description: Option<String>,
}
#[post("/register")]
#[tracing::instrument]
async fn save_account_details(
@ -245,12 +257,11 @@ async fn save_account_details(
db: Data<PgPool>,
id: Identity,
) -> HttpResult {
let mut form = form.into_inner();
let form = form.into_inner();
dbg!(&form);
process_items(form.items.get_or_insert_default(), form.names);
let pool = db.into_inner();
if form.account_type == db::AccountType::Admin {
if form.account_type == AccountType::Admin {
return HttpResult::err(&req, routes::Error::XSS);
}
@ -300,6 +311,48 @@ async fn save_account_details(
}
};
if let Err(e) = queries::create_contact(
&mut t,
db::CreateContactInput {
owner_id: account.id,
contact_type: "email".into(),
content: account.email.clone(),
},
)
.await
{
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Problem z utworzeniem danych kontaktowych konta",
Page::Register,
);
}
if let Some(contacts) = form.contacts {
for contact in contacts {
if let Err(e) = queries::create_contact(
&mut t,
db::CreateContactInput {
owner_id: account.id,
contact_type: contact.contact_type,
content: contact.content,
},
)
.await
{
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
&req,
"Problem z utworzeniem danych kontaktowych konta",
Page::Register,
);
}
}
}
if matches!(form.account_type, db::AccountType::Business) {
let name = form.name.as_deref().unwrap_or_default();
let owner_id = account.id;
@ -325,23 +378,13 @@ async fn save_account_details(
for (idx, item) in form.items.unwrap_or_default().into_iter().enumerate() {
not_xss!(&req, &item.name, t);
not_xss!(&req, &item.picture_url, t);
not_xss!(&req, item.picture_url.as_deref().unwrap_or_default(), t);
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
r#"
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, local_business_id, name, price, item_order, picture_url
"#,
)
.bind(business.id)
.bind(&item.name)
.bind(item.price as i32)
.bind(idx as i32)
.bind(if item.picture_url.is_empty() {
let picture_url = match item.picture_url {
Some(url) => Some(if url.is_empty() {
format!("--{}", uuid::Uuid::new_v4())
} else {
let name = item.picture_url.split('/').last().unwrap_or_default();
let name = url.split('/').last().unwrap_or_default();
let dir = utils::item_picture_write_dir(format!("{}", account.id));
if let Err(e) = std::fs::create_dir_all(&dir) {
error!("{e} {:?}", dir);
@ -354,8 +397,8 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
);
}
let path = dir.join(name);
if let Err(e) = std::fs::rename(format!(".{}", item.picture_url), &path) {
error!("{e} {:?}", item.picture_url);
if let Err(e) = std::fs::rename(format!(".{}", url), &path) {
error!("{e} {:?}", url);
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
@ -368,13 +411,24 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
path.strip_prefix('.')
.map(String::from)
.unwrap_or_else(|| path)
})
.fetch_one(&mut t)
}),
None => None,
};
let res = queries::create_item(
&mut t,
db::CreateLocalBusinessItemInput {
local_business_id: business.id,
name: item.name,
price: item.price as i64,
item_order: idx as i32,
picture_url,
},
)
.await;
match res {
Ok(_) => {}
Err(e) => {
error!("{e}");
error!("{e:?}");
dbg!(e);
t.rollback().await.unwrap();
return RegisterTemplate::bad_request(
@ -427,68 +481,6 @@ async fn display_register_page(
)
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
struct RegisterForm {
email: String,
login: String,
password: String,
facebook_id: Option<String>,
account_type: db::AccountType,
items: Option<Vec<view::BusinessItemInput>>,
contacts: Option<Vec<view::CreateContactInfoInput>>,
name: Option<String>,
description: Option<String>,
#[serde(flatten)]
names: HashMap<String, String>,
}
#[tracing::instrument]
fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String, String>) {
let mut h = names
.into_iter()
.filter_map(|(name, value)| {
let mut name = name
.strip_prefix("items")?
.split('[')
.filter(|s| !s.is_empty())
.map(|s| s.strip_suffix(']').unwrap_or(s));
let idx: u16 = name.next().and_then(|s| s.parse().ok())?;
match name.next() {
Some(s @ ("name" | "price" | "picture_url")) => Some((idx, s.to_string(), value)),
_ => None,
}
})
.fold(
HashMap::with_capacity(60),
|mut memo, (idx, field, value)| {
let item = memo
.entry(idx)
.or_insert_with(view::BusinessItemInput::default);
match field.as_str() {
"name" => {
item.name = value;
}
"price" => {
item.price = value.parse().unwrap_or_default();
}
"picture_url" => {
item.picture_url = value;
}
_ => {}
};
memo
},
);
let mut ids = { h.keys().copied().collect::<Vec<_>>() };
ids.sort();
for id in ids {
if let Some(item) = h.remove(&id) {
items.push(item);
}
}
}
#[post("/upload")]
async fn upload(
req: HttpRequest,
@ -523,30 +515,4 @@ mod tests {
use crate::model::view;
use crate::model::view::BusinessItemInput;
impl BusinessItemInput {
pub fn new<S: Into<String>, P: Into<String>>(name: S, price: u32, picture_url: P) -> Self {
Self {
name: name.into(),
price,
picture_url: picture_url.into(),
}
}
}
#[test]
fn parse_items() {
let mut items = Vec::with_capacity(0);
let mut names: HashMap<String, String> = HashMap::with_capacity(4);
names.insert("items[0][name]".into(), "a".into());
names.insert("items[0][price]".into(), "10".into());
names.insert("items[1][name]".into(), "b".into());
names.insert("items[1][price]".into(), "20".into());
super::process_items(&mut items, names);
let expected = vec![
BusinessItemInput::new("a", 10, "/a"),
BusinessItemInput::new("b", 20, "/b"),
];
assert_eq!(items, expected);
}
}

View File

@ -9,7 +9,7 @@ use crate::queries;
use crate::routes::unrestricted::IndexTemplate;
use crate::routes::{HttpResult, Identity};
#[get("/")]
#[get("/local-businesses")]
#[tracing::instrument]
pub async fn businesses_page(req: HttpRequest, db: Data<PgPool>, id: Identity) -> HttpResult {
let pool = db.into_inner();

View File

@ -73,6 +73,82 @@ async fn marketplace(req: HttpRequest, db: Data<PgPool>, id: Identity) -> HttpRe
)
}
pub fn configure(config: &mut ServiceConfig) {
config.service(web::scope("/marketplace").service(marketplace));
#[derive(Default, Serialize, Template)]
#[template(path = "./marketplace/edit.html")]
struct EditOfferTemplate {
account: Option<db::Account>,
error: Option<String>,
page: Page,
#[serde(skip)]
h: Helper,
offer: view::Offer,
}
#[get("/{id}/edit")]
async fn edit_marketplace(
req: HttpRequest,
db: Data<PgPool>,
id: Identity,
path: web::Path<(i32,)>,
) -> HttpResult {
let offer_id = path.into_inner().0;
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(&req, pool.begin().await);
let account = match id.identity() {
Some(id) => queries::account_by_id(&mut t, id).await,
_ => None,
};
let account = match account {
Some(a) => a,
_ => {
return HttpResult::res(
&req,
StatusCode::NOT_FOUND,
EditOfferTemplate {
page: Page::Marketplace,
account,
error: Some("Oferta nie istnieje".into()),
..Default::default()
},
)
}
};
let offer = match queries::offer_by_id(&mut t, account.id, offer_id).await {
Ok(offer) => offer,
Err(e) => {
dbg!(e);
return HttpResult::res(
&req,
StatusCode::NOT_FOUND,
EditOfferTemplate {
page: Page::Marketplace,
account: Some(account),
error: Some("Oferta nie istnieje".into()),
..Default::default()
},
);
}
};
t.commit().await.ok();
HttpResult::res(
&req,
StatusCode::OK,
EditOfferTemplate {
page: Page::Marketplace,
account: Some(account),
offer: view::Offer::from((offer, &vec![])),
..Default::default()
},
)
}
pub fn configure(config: &mut ServiceConfig) {
config.service(
web::scope("/marketplace")
.service(edit_marketplace)
.service(marketplace),
);
}