Admin, manage offers

This commit is contained in:
Adrian Woźniak 2022-07-21 15:06:14 +02:00
parent d29326e76e
commit c4766774dc
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
38 changed files with 2414 additions and 1538 deletions

108
Cargo.lock generated
View File

@ -433,6 +433,17 @@ dependencies = [
"toml",
]
[[package]]
name = "async-trait"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atoi"
version = "1.0.0"
@ -747,6 +758,12 @@ version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "firestorm"
version = "0.5.1"
@ -1314,6 +1331,7 @@ dependencies = [
"futures-util",
"gumdrop",
"password-hash",
"postgres",
"rand",
"serde",
"serde_json",
@ -1397,6 +1415,24 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "phf"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.0.11"
@ -1441,6 +1477,49 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "postgres"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8bbcd5f6deb39585a0d9f4ef34c4a41c25b7ad26d23c75d837d78c8e7adc85f"
dependencies = [
"bytes",
"fallible-iterator",
"futures",
"log",
"tokio",
"tokio-postgres",
]
[[package]]
name = "postgres-protocol"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c"
dependencies = [
"base64",
"byteorder",
"bytes",
"fallible-iterator",
"hmac",
"md-5",
"memchr",
"rand",
"sha2",
"stringprep",
]
[[package]]
name = "postgres-types"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd6e8b7189a73169290e89bd24c771071f1012d8fe6f738f5226531f0b03d89"
dependencies = [
"bytes",
"fallible-iterator",
"postgres-protocol",
]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
@ -1723,6 +1802,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "slab"
version = "0.4.6"
@ -1977,6 +2062,29 @@ dependencies = [
"winapi",
]
[[package]]
name = "tokio-postgres"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19c88a47a23c5d2dc9ecd28fb38fba5fc7e5ddc1fe64488ec145076b0c71c8ae"
dependencies = [
"async-trait",
"byteorder",
"bytes",
"fallible-iterator",
"futures",
"log",
"parking_lot 0.12.1",
"percent-encoding",
"phf",
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"socket2",
"tokio",
"tokio-util",
]
[[package]]
name = "tokio-rustls"
version = "0.23.4"

View File

@ -21,6 +21,7 @@ futures = { version = "0.3.21", features = ["async-await", "std"] }
futures-util = { version = "0.3.21", features = [] }
gumdrop = { version = "*" }
password-hash = { version = "0.4.2" }
postgres = { version = "0.19.3" }
rand = { version = "0.8.5", features = [] }
serde = { version = "*", features = ["derive"] }
serde_json = { version = "*" }

View File

@ -1,3 +1,3 @@
[general]
dirs = ["assets/templates"]
whitespace = "preserve"
whitespace = "suppress"

View File

@ -1,8 +1,8 @@
{% extends "layout.html" %}
{% block content %}
{% block content -%}
<ow-admin>
<admin-businesses>
{% for business in businesses %}
{% for business in businesses -%}
<admin-edit-business business-id="{{ business.id }}" state="{{ business.state }}">
<admin-business
business-id="{{ business.id }}"
@ -11,7 +11,7 @@
>
<p slot="description">{{business.description|safe}}</p>
{% for item in business.items %}
{% for item in business.items -%}
<local-business-item
slot="item"
name="{{item.name}}"
@ -19,10 +19,10 @@
picture-url="{{item.picture_url}}"
>
</local-business-item>
{% endfor %}
{%- endfor %}
</admin-business>
</admin-edit-business>
{% endfor %}
{%- endfor %}
</admin-businesses>
</ow-admin>
{% endblock %}
{%- endblock %}

View File

@ -7,6 +7,6 @@
published-at="{{article.published_at|opt_time}}"
created-at="{{article.created_at}}"
>
{{ article.body|safe }}
{{- article.body|safe -}}
</article-form>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% block content %}
<ow-admin>
<ow-articles>
{% for article in news %}
{% for article in news -%}
<edit-news-article article-id="{{ article.id }}">
<news-article
article-id="{{ article.id }}"
@ -11,10 +11,10 @@
published-at="{{ article.published_at|opt_time }}"
created-at="{{ article.created_at }}"
>
{{article.body|safe}}
{{- article.body|safe -}}
</news-article>
</edit-news-article>
{% endfor %}
{%- endfor %}
</ow-articles>
<article-form></article-form>
</ow-admin>

View File

@ -0,0 +1,45 @@
{% extends "../layout.html" %}
{% block content %}
<ow-admin>
<ow-offers>
<h1>Admin - Sprzedaż niepotrzebnych rzeczy</h1>
{% for offer in offers %}
<admin-edit-offer
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 %}
>
<marketplace-offer
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 %}
></marketplace-offer>
</admin-edit-offer>
{% endfor %}
</ow-offers>
</ow-admin>
{% endblock %}

View File

@ -19,99 +19,7 @@
{% when None %}
{% endmatch %}
<article>
<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>
{% if page.is_public() %}
<ow-path path="/" selected="{{ page.select_index() }}" title="Lokalne usługi">
<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="/marketplace" selected="{{ page.select_marketplace() }}" title="Targ">
<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"/>
<path d="M466.406 272.568h-14.13a28.036 28.036 0 0 0 6.552-18.05c0-15.549-12.604-28.154-28.153-28.154s-28.154 12.605-28.154 28.154c0 6.87 2.464 13.163 6.553 18.05h-22.131a28.031 28.031 0 0 0 6.553-18.05c0-15.549-12.604-28.154-28.154-28.154-15.549 0-28.153 12.605-28.153 28.154 0 6.87 2.464 13.163 6.553 18.05h-22.491a28.036 28.036 0 0 0 6.552-18.05c0-2.477-.322-4.877-.923-7.165a27.356 27.356 0 0 0-8.209-13.587 28.05 28.05 0 0 0-19.022-7.402c-15.549 0-28.154 12.605-28.154 28.154 0 6.87 2.464 13.163 6.553 18.05H112.007c-10.22 0-18.504 8.284-18.504 18.495s8.284 18.495 18.504 18.495h354.399c10.22 0 18.503-8.284 18.503-18.495s-8.283-18.495-18.503-18.495z"/>
<path d="M370.467 205.351c0 15.115 12.25 27.373 27.374 27.373 15.121 0 27.371-12.258 27.371-27.373 0-15.115-12.25-27.373-27.371-27.373-15.124 0-27.374 12.258-27.374 27.373zm-5.125-21.374c15.122 0 27.372-12.258 27.372-27.373 0-15.115-12.25-27.373-27.372-27.373-15.123 0-27.373 12.258-27.373 27.373 0 15.114 12.25 27.373 27.373 27.373zm-32.498 48.747c15.122 0 27.372-12.258 27.372-27.373 0-15.115-12.25-27.373-27.372-27.373-15.123 0-27.373 12.258-27.373 27.373 0 15.114 12.25 27.373 27.373 27.373z"/>
</svg>
<div>Targ</div>
</ow-path>
<ow-path path="/news" selected="{{ page.select_news() }}" title="Aktualności">
<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"/>
<path d="M383.4 108.6c-5.8 0-10.6-4.7-10.6-10.6 0-5.8 4.7-10.6 10.6-10.6 2.9 0 5.7 1.2 7.7 3.3 2.7 2.8 7.1 3 10 .3 2.8-2.7 3-7.1.3-10-4.6-4.9-11.2-7.8-18-7.8-13.6 0-24.7 11.1-24.7 24.7s11.1 24.7 24.7 24.7c5.8 0 10.6 4.7 10.6 10.6 0 5.8-4.7 10.6-10.6 10.6-2.9 0-5.7-1.2-7.7-3.3-2.7-2.8-7.1-3-10-.3-2.8 2.7-3 7.1-.3 10 4.6 4.9 11.2 7.8 18 7.8 13.6 0 24.7-11.1 24.7-24.7s-11.1-24.7-24.7-24.7zM221.5 123c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-20.8V87.9h20.8c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-27.8c-3.9 0-7.1 3.2-7.1 7.1v70.5c.1 3.9 3.2 7.1 7.1 7.1h27.8c3.9 0 7.1-3.2 7.1-7.1s-3.2-7.1-7.1-7.1h-20.8V123h20.8zM149.4 73.7c-3.9 0-7.1 3.2-7.1 7.1v45L113 77.2c-1.6-2.7-4.9-4-7.9-3.2-3.1.8-5.2 3.6-5.2 6.8v70.5c0 3.9 3.2 7.1 7.1 7.1s7.1-3.2 7.1-7.1v-45l29.2 48.7c1.3 2.2 3.6 3.4 6.1 3.4.6 0 1.3-.1 1.9-.3 3.1-.8 5.2-3.6 5.2-6.8V80.8c0-3.9-3.2-7.1-7.1-7.1zM335.1 74.1c-3.8-1-7.6 1.2-8.6 5l-12.1 45S302.1 78.7 302 78.5c-.8-2.1-2.4-3.8-4.7-4.4-3.8-1-7.6 1.2-8.6 5l-12.1 45-12-45c-1-3.8-4.9-6-8.6-5-3.8 1-6 4.9-5 8.6l18.9 70.5c.8 3.1 3.6 5.2 6.8 5.2s6-2.1 6.8-5.2l12-45 12.1 45c.8 3.1 3.6 5.2 6.8 5.2 3.2 0 6-2.1 6.8-5.2l18.9-70.5c1-3.8-1.2-7.6-5-8.6z"/>
</svg>
<div>Aktualności</div>
</ow-path>
<ow-path path="/account" selected="{{ page.select_account() }}" title="Konto">
<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>
<div>Konto</div>
</ow-path>
{% if h.is_above_user(account) %}
<ow-path path="/account/business-items" selected="{{ page.select_account_business() }}" title="Moje usługi">
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M25 26a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V5h10V3H5v23a3 3 0 0 0 3 3h16a3 3 0 0 0 3-3V13h-2Z"/>
<path d="M27.12 2.88a3.08 3.08 0 0 0-4.24 0L17 8.75l-1 5.3L21.25 13l5.87-5.87a3 3 0 0 0 0-4.25Zm-6.86 8.27-1.76.35.35-1.76 3.32-3.33 1.42 1.42Zm5.45-5.44-.71.7L23.59 5l.7-.71a1 1 0 0 1 1.42 0 1 1 0 0 1 0 1.42Z"/>
</svg>
<div>Moje usługi</div>
</ow-path>
{% endif %}
<ow-path path="/account/offers" selected="{{ page.select_account_offers() }}" title="Moje sprzedaże">
<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"
/>
<path d="M17.52 15.715c-.638-.713-5.53-6.169-7.115-7.557 1.257-.794 2.022-2.14 2.022-3.61C12.427 2.168 10.42.23 7.952.23c-2.468 0-4.475 1.938-4.475 4.32 0 1.455.75 2.789 1.986 3.586-1.026.75-1.648 1.93-1.648 3.186v18.466c0 .971.819 1.761 1.825 1.761s1.824-.79 1.824-1.76V19.643h.938v10.143c0 .971.819 1.761 1.825 1.761s1.824-.79 1.824-1.76l.036-15.054 3.216 3.104c.304.294.704.44 1.104.44.4 0 .8-.146 1.104-.44a1.472 1.472 0 0 0 .008-2.123z"
/>
</svg>
</ow-path>
{% if h.is_admin(account) %}
<ow-path path="/admin" selected="{{ page.select_admin_news() }}" title="Admin">
<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"/>
<path d="M26.87 16.29a.37.37 0 0 1 .15 0 .42.42 0 0 0-.15 0Z"/>
<path d="m33.68 23.32-2-.61a7.21 7.21 0 0 0-.58-1.41l1-1.86A.38.38 0 0 0 32 19l-1.45-1.45a.36.36 0 0 0-.44-.07l-1.84 1a7.15 7.15 0 0 0-1.43-.61l-.61-2a.36.36 0 0 0-.36-.24h-2.05a.36.36 0 0 0-.35.26l-.61 2a7 7 0 0 0-1.44.6l-1.82-1a.35.35 0 0 0-.43.07L17.69 19a.38.38 0 0 0-.06.44l1 1.82a6.77 6.77 0 0 0-.63 1.43l-2 .6a.36.36 0 0 0-.26.35v2.05A.35.35 0 0 0 16 26l2 .61a7 7 0 0 0 .6 1.41l-1 1.91a.36.36 0 0 0 .06.43l1.45 1.45a.38.38 0 0 0 .44.07l1.87-1a7.09 7.09 0 0 0 1.4.57l.6 2a.38.38 0 0 0 .35.26h2.05a.37.37 0 0 0 .35-.26l.61-2.05a6.92 6.92 0 0 0 1.38-.57l1.89 1a.36.36 0 0 0 .43-.07L32 30.4a.35.35 0 0 0 0-.4l-1-1.88a7 7 0 0 0 .58-1.39l2-.61a.36.36 0 0 0 .26-.35v-2.1a.36.36 0 0 0-.16-.35ZM24.85 28a3.34 3.34 0 1 1 3.33-3.33A3.34 3.34 0 0 1 24.85 28Z"/>
<path fill="none" d="M0 0h36v36H0z"/>
</svg>
<div>Admin</div>
</ow-path>
{% endif %}
{% else if page.is_admin() %}
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}" title="Zarządzaj aktualnościami">
<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"/>
<path d="M383.4 108.6c-5.8 0-10.6-4.7-10.6-10.6 0-5.8 4.7-10.6 10.6-10.6 2.9 0 5.7 1.2 7.7 3.3 2.7 2.8 7.1 3 10 .3 2.8-2.7 3-7.1.3-10-4.6-4.9-11.2-7.8-18-7.8-13.6 0-24.7 11.1-24.7 24.7s11.1 24.7 24.7 24.7c5.8 0 10.6 4.7 10.6 10.6 0 5.8-4.7 10.6-10.6 10.6-2.9 0-5.7-1.2-7.7-3.3-2.7-2.8-7.1-3-10-.3-2.8 2.7-3 7.1-.3 10 4.6 4.9 11.2 7.8 18 7.8 13.6 0 24.7-11.1 24.7-24.7s-11.1-24.7-24.7-24.7zM221.5 123c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-20.8V87.9h20.8c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-27.8c-3.9 0-7.1 3.2-7.1 7.1v70.5c.1 3.9 3.2 7.1 7.1 7.1h27.8c3.9 0 7.1-3.2 7.1-7.1s-3.2-7.1-7.1-7.1h-20.8V123h20.8zM149.4 73.7c-3.9 0-7.1 3.2-7.1 7.1v45L113 77.2c-1.6-2.7-4.9-4-7.9-3.2-3.1.8-5.2 3.6-5.2 6.8v70.5c0 3.9 3.2 7.1 7.1 7.1s7.1-3.2 7.1-7.1v-45l29.2 48.7c1.3 2.2 3.6 3.4 6.1 3.4.6 0 1.3-.1 1.9-.3 3.1-.8 5.2-3.6 5.2-6.8V80.8c0-3.9-3.2-7.1-7.1-7.1zM335.1 74.1c-3.8-1-7.6 1.2-8.6 5l-12.1 45S302.1 78.7 302 78.5c-.8-2.1-2.4-3.8-4.7-4.4-3.8-1-7.6 1.2-8.6 5l-12.1 45-12-45c-1-3.8-4.9-6-8.6-5-3.8 1-6 4.9-5 8.6l18.9 70.5c.8 3.1 3.6 5.2 6.8 5.2s6-2.1 6.8-5.2l12-45 12.1 45c.8 3.1 3.6 5.2 6.8 5.2 3.2 0 6-2.1 6.8-5.2l18.9-70.5c1-3.8-1.2-7.6-5-8.6z"/>
</svg>
<div>Aktualności</div>
</ow-path>
<ow-path path="/admin/businesses" selected="{{ page.select_admin_businesses() }}"
title="Zarządzaj usługami">
<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>
{% endif %}
</ow-nav>
{% include "nav.html" %}
{% block content %}{% endblock %}
</article>
</main>

109
assets/templates/nav.html Normal file
View File

@ -0,0 +1,109 @@
<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>
{% if page.is_public() -%}
<ow-path path="/" selected="{{ page.select_index() }}" title="Lokalne usługi">
<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="/marketplace" selected="{{ page.select_marketplace() }}" title="Targ">
<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"/>
<path d="M466.406 272.568h-14.13a28.036 28.036 0 0 0 6.552-18.05c0-15.549-12.604-28.154-28.153-28.154s-28.154 12.605-28.154 28.154c0 6.87 2.464 13.163 6.553 18.05h-22.131a28.031 28.031 0 0 0 6.553-18.05c0-15.549-12.604-28.154-28.154-28.154-15.549 0-28.153 12.605-28.153 28.154 0 6.87 2.464 13.163 6.553 18.05h-22.491a28.036 28.036 0 0 0 6.552-18.05c0-2.477-.322-4.877-.923-7.165a27.356 27.356 0 0 0-8.209-13.587 28.05 28.05 0 0 0-19.022-7.402c-15.549 0-28.154 12.605-28.154 28.154 0 6.87 2.464 13.163 6.553 18.05H112.007c-10.22 0-18.504 8.284-18.504 18.495s8.284 18.495 18.504 18.495h354.399c10.22 0 18.503-8.284 18.503-18.495s-8.283-18.495-18.503-18.495z"/>
<path d="M370.467 205.351c0 15.115 12.25 27.373 27.374 27.373 15.121 0 27.371-12.258 27.371-27.373 0-15.115-12.25-27.373-27.371-27.373-15.124 0-27.374 12.258-27.374 27.373zm-5.125-21.374c15.122 0 27.372-12.258 27.372-27.373 0-15.115-12.25-27.373-27.372-27.373-15.123 0-27.373 12.258-27.373 27.373 0 15.114 12.25 27.373 27.373 27.373zm-32.498 48.747c15.122 0 27.372-12.258 27.372-27.373 0-15.115-12.25-27.373-27.372-27.373-15.123 0-27.373 12.258-27.373 27.373 0 15.114 12.25 27.373 27.373 27.373z"/>
</svg>
<div>Targ</div>
</ow-path>
<ow-path path="/news" selected="{{ page.select_news() }}" title="Aktualności">
<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"/>
<path d="M383.4 108.6c-5.8 0-10.6-4.7-10.6-10.6 0-5.8 4.7-10.6 10.6-10.6 2.9 0 5.7 1.2 7.7 3.3 2.7 2.8 7.1 3 10 .3 2.8-2.7 3-7.1.3-10-4.6-4.9-11.2-7.8-18-7.8-13.6 0-24.7 11.1-24.7 24.7s11.1 24.7 24.7 24.7c5.8 0 10.6 4.7 10.6 10.6 0 5.8-4.7 10.6-10.6 10.6-2.9 0-5.7-1.2-7.7-3.3-2.7-2.8-7.1-3-10-.3-2.8 2.7-3 7.1-.3 10 4.6 4.9 11.2 7.8 18 7.8 13.6 0 24.7-11.1 24.7-24.7s-11.1-24.7-24.7-24.7zM221.5 123c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-20.8V87.9h20.8c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-27.8c-3.9 0-7.1 3.2-7.1 7.1v70.5c.1 3.9 3.2 7.1 7.1 7.1h27.8c3.9 0 7.1-3.2 7.1-7.1s-3.2-7.1-7.1-7.1h-20.8V123h20.8zM149.4 73.7c-3.9 0-7.1 3.2-7.1 7.1v45L113 77.2c-1.6-2.7-4.9-4-7.9-3.2-3.1.8-5.2 3.6-5.2 6.8v70.5c0 3.9 3.2 7.1 7.1 7.1s7.1-3.2 7.1-7.1v-45l29.2 48.7c1.3 2.2 3.6 3.4 6.1 3.4.6 0 1.3-.1 1.9-.3 3.1-.8 5.2-3.6 5.2-6.8V80.8c0-3.9-3.2-7.1-7.1-7.1zM335.1 74.1c-3.8-1-7.6 1.2-8.6 5l-12.1 45S302.1 78.7 302 78.5c-.8-2.1-2.4-3.8-4.7-4.4-3.8-1-7.6 1.2-8.6 5l-12.1 45-12-45c-1-3.8-4.9-6-8.6-5-3.8 1-6 4.9-5 8.6l18.9 70.5c.8 3.1 3.6 5.2 6.8 5.2s6-2.1 6.8-5.2l12-45 12.1 45c.8 3.1 3.6 5.2 6.8 5.2 3.2 0 6-2.1 6.8-5.2l18.9-70.5c1-3.8-1.2-7.6-5-8.6z"/>
</svg>
<div>Aktualności</div>
</ow-path>
<ow-path path="/account" selected="{{ page.select_account() }}" title="Konto">
<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>
<div>Konto</div>
</ow-path>
{% if h.is_above_user(account) -%}
<ow-path path="/account/business-items" selected="{{ page.select_account_business() }}" title="Moje usługi">
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M25 26a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V5h10V3H5v23a3 3 0 0 0 3 3h16a3 3 0 0 0 3-3V13h-2Z"/>
<path d="M27.12 2.88a3.08 3.08 0 0 0-4.24 0L17 8.75l-1 5.3L21.25 13l5.87-5.87a3 3 0 0 0 0-4.25Zm-6.86 8.27-1.76.35.35-1.76 3.32-3.33 1.42 1.42Zm5.45-5.44-.71.7L23.59 5l.7-.71a1 1 0 0 1 1.42 0 1 1 0 0 1 0 1.42Z"/>
</svg>
<div>Moje usługi</div>
</ow-path>
{%- endif %}
<ow-path path="/account/offers" selected="{{ page.select_account_offers() }}" title="Moje sprzedaże">
<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"
/>
<path
d="M17.52 15.715c-.638-.713-5.53-6.169-7.115-7.557 1.257-.794 2.022-2.14 2.022-3.61C12.427 2.168 10.42.23 7.952.23c-2.468 0-4.475 1.938-4.475 4.32 0 1.455.75 2.789 1.986 3.586-1.026.75-1.648 1.93-1.648 3.186v18.466c0 .971.819 1.761 1.825 1.761s1.824-.79 1.824-1.76V19.643h.938v10.143c0 .971.819 1.761 1.825 1.761s1.824-.79 1.824-1.76l.036-15.054 3.216 3.104c.304.294.704.44 1.104.44.4 0 .8-.146 1.104-.44a1.472 1.472 0 0 0 .008-2.123z"
/>
</svg>
</ow-path>
{% if h.is_admin(account) -%}
<ow-path path="/admin" selected="{{ page.select_admin_news() }}" title="Admin">
<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"/>
<path d="M26.87 16.29a.37.37 0 0 1 .15 0 .42.42 0 0 0-.15 0Z"/>
<path d="m33.68 23.32-2-.61a7.21 7.21 0 0 0-.58-1.41l1-1.86A.38.38 0 0 0 32 19l-1.45-1.45a.36.36 0 0 0-.44-.07l-1.84 1a7.15 7.15 0 0 0-1.43-.61l-.61-2a.36.36 0 0 0-.36-.24h-2.05a.36.36 0 0 0-.35.26l-.61 2a7 7 0 0 0-1.44.6l-1.82-1a.35.35 0 0 0-.43.07L17.69 19a.38.38 0 0 0-.06.44l1 1.82a6.77 6.77 0 0 0-.63 1.43l-2 .6a.36.36 0 0 0-.26.35v2.05A.35.35 0 0 0 16 26l2 .61a7 7 0 0 0 .6 1.41l-1 1.91a.36.36 0 0 0 .06.43l1.45 1.45a.38.38 0 0 0 .44.07l1.87-1a7.09 7.09 0 0 0 1.4.57l.6 2a.38.38 0 0 0 .35.26h2.05a.37.37 0 0 0 .35-.26l.61-2.05a6.92 6.92 0 0 0 1.38-.57l1.89 1a.36.36 0 0 0 .43-.07L32 30.4a.35.35 0 0 0 0-.4l-1-1.88a7 7 0 0 0 .58-1.39l2-.61a.36.36 0 0 0 .26-.35v-2.1a.36.36 0 0 0-.16-.35ZM24.85 28a3.34 3.34 0 1 1 3.33-3.33A3.34 3.34 0 0 1 24.85 28Z"/>
<path fill="none" d="M0 0h36v36H0z"/>
</svg>
<div>Admin</div>
</ow-path>
{%- endif %}
{% else if page.is_admin() -%}
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}" title="Zarządzaj aktualnościami">
<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"/>
<path d="M383.4 108.6c-5.8 0-10.6-4.7-10.6-10.6 0-5.8 4.7-10.6 10.6-10.6 2.9 0 5.7 1.2 7.7 3.3 2.7 2.8 7.1 3 10 .3 2.8-2.7 3-7.1.3-10-4.6-4.9-11.2-7.8-18-7.8-13.6 0-24.7 11.1-24.7 24.7s11.1 24.7 24.7 24.7c5.8 0 10.6 4.7 10.6 10.6 0 5.8-4.7 10.6-10.6 10.6-2.9 0-5.7-1.2-7.7-3.3-2.7-2.8-7.1-3-10-.3-2.8 2.7-3 7.1-.3 10 4.6 4.9 11.2 7.8 18 7.8 13.6 0 24.7-11.1 24.7-24.7s-11.1-24.7-24.7-24.7zM221.5 123c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-20.8V87.9h20.8c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-27.8c-3.9 0-7.1 3.2-7.1 7.1v70.5c.1 3.9 3.2 7.1 7.1 7.1h27.8c3.9 0 7.1-3.2 7.1-7.1s-3.2-7.1-7.1-7.1h-20.8V123h20.8zM149.4 73.7c-3.9 0-7.1 3.2-7.1 7.1v45L113 77.2c-1.6-2.7-4.9-4-7.9-3.2-3.1.8-5.2 3.6-5.2 6.8v70.5c0 3.9 3.2 7.1 7.1 7.1s7.1-3.2 7.1-7.1v-45l29.2 48.7c1.3 2.2 3.6 3.4 6.1 3.4.6 0 1.3-.1 1.9-.3 3.1-.8 5.2-3.6 5.2-6.8V80.8c0-3.9-3.2-7.1-7.1-7.1zM335.1 74.1c-3.8-1-7.6 1.2-8.6 5l-12.1 45S302.1 78.7 302 78.5c-.8-2.1-2.4-3.8-4.7-4.4-3.8-1-7.6 1.2-8.6 5l-12.1 45-12-45c-1-3.8-4.9-6-8.6-5-3.8 1-6 4.9-5 8.6l18.9 70.5c.8 3.1 3.6 5.2 6.8 5.2s6-2.1 6.8-5.2l12-45 12.1 45c.8 3.1 3.6 5.2 6.8 5.2 3.2 0 6-2.1 6.8-5.2l18.9-70.5c1-3.8-1.2-7.6-5-8.6z"/>
</svg>
<div>Aktualności</div>
</ow-path>
<ow-path path="/admin/businesses" selected="{{ page.select_admin_businesses() }}"
title="Zarządzaj usługami">
<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="/admin/offers" selected="{{ page.select_admin_offers() }}"
title="Zarządzaj ofertami">
<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"
/>
<path
d="M17.52 15.715c-.638-.713-5.53-6.169-7.115-7.557 1.257-.794 2.022-2.14 2.022-3.61C12.427 2.168 10.42.23 7.952.23c-2.468 0-4.475 1.938-4.475 4.32 0 1.455.75 2.789 1.986 3.586-1.026.75-1.648 1.93-1.648 3.186v18.466c0 .971.819 1.761 1.825 1.761s1.824-.79 1.824-1.76V19.643h.938v10.143c0 .971.819 1.761 1.825 1.761s1.824-.79 1.824-1.76l.036-15.054 3.216 3.104c.304.294.704.44 1.104.44.4 0 .8-.146 1.104-.44a1.472 1.472 0 0 0 .008-2.123z"
/>
</svg>
<div>Zarządzań ofertami</div>
</ow-path>
{%- endif %}
</ow-nav>

View File

@ -5,20 +5,40 @@
<offer-form></offer-form>
{% for offer in offers %}
<user-edit-offer
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 %}
>
<marketplace-offer
offer-id="{{offer.id}}"
description="{{offer.description}}"
picture-url="{{offer.picture_url}}"
{% match offer.price_range %}
{% when PriceRange::Free %}
price-range="free"
price-range-min="0"
price-range-max="0"
{% when PriceRange::Fixed with { value } %}
price-range="{{value}}"
price-range-min="{{value}}"
price-range-max="0"
{% when PriceRange::Range with { min, max } %}
price-range-min="{{min}}"
price-range-max="{{max}}"
{% endmatch %}
></marketplace-offer>
</user-edit-offer>
{% endfor %}
</marketplace-offers>
{% endblock %}

View File

@ -1,8 +1,10 @@
import "./admin/ow-admin";
import "./admin/article-form";
import "./admin/edit-news-article";
import "./admin/news/article-form";
import "./admin/news/edit-news-article";
import "./admin/admin-business";
import "./admin/admin-businesses";
import "./admin/admin-edit-business";
import "./admin/businesses/admin-business";
import "./admin/businesses/admin-businesses";
import "./admin/businesses/admin-edit-business";
import "./admin/offers/admin-edit-offer";

View File

@ -1,4 +1,4 @@
import { Component } from "../shared";
import { Component } from "../../shared";
customElements.define('admin-business', class extends Component {
static get observedAttributes() {

View File

@ -1,4 +1,4 @@
import { Component } from "../shared";
import { Component } from "../../shared";
customElements.define('admin-businesses', class extends Component {
constructor() {

View File

@ -1,4 +1,4 @@
import { Component, BUTTON_STYLE, INPUT_STYLE } from "../shared";
import { Component, BUTTON_STYLE, INPUT_STYLE } from "../../shared";
customElements.define('admin-edit-business', class extends Component {
static get observedAttributes() {

View File

@ -1,4 +1,4 @@
import { Component, FORM_STYLE } from "../shared";
import { Component, FORM_STYLE } from "../../shared";
customElements.define('article-form', class extends Component {
static get observedAttributes() {

View File

@ -1,4 +1,4 @@
import { Component, BUTTON_STYLE } from "../shared";
import { Component, BUTTON_STYLE } from "../../shared";
customElements.define('edit-news-article', class extends Component {
static get observedAttributes() {

View File

@ -0,0 +1,109 @@
import { Component, BUTTON_STYLE } from "../../shared";
customElements.define('admin-edit-offer', class extends Component {
static get observedAttributes() {
return ['offer-id', 'state', 'description', 'picture-url', 'price-range-min', 'price-range-max'];
}
constructor() {
super(`
<style>
:host {
display: block;
}
#view { display: block; }
#actions > input {
width: 100%;
}
#state {
font-weight: bold;
margin: 8px;
}
:host([state="approved"]) #state { color: green; }
:host([state="banned"]) #state { color: darkred; }
:host([state="pending"]) #state { color: orange; }
:host([state="approved"]) #approve { display: none; }
:host([state="banned"]) #ban { display: none; }
@media only screen and (min-device-width: 1200px) {
#view { display: flex; }
#actions {
margin-left: 16px;
width: 120px;
}
}
${BUTTON_STYLE}
</style>
<section id="view">
<slot></slot>
<div id="actions">
<div id="state"></div>
<form action="/admin/offers/approve" method="post">
<input type="hidden" name="id" class="offer_id" />
<input type="submit" id="approve" value="Zaakceptuj" />
</form>
<form action="/admin/offers/ban" method="post">
<input type="hidden" name="id" class="offer_id" />
<input type="submit" id="ban" value="Zbanuj" />
</form>
</div>
</section>
`);
}
get offer_id() {
const v = parseInt(this.getAttribute('offer-id'));
return isNaN(v) ? null : v;
}
set offer_id(v) {
v = parseInt(v);
this.setAttribute('offer-id', v);
for (const el of this.shadowRoot.querySelectorAll('.offer_id')) {
el.value = v;
}
}
get description() {
return this.getAttribute('description');
}
set description(v) {
this.setAttribute('description', v);
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
this.setAttribute('picture-url', v);
}
get price_range_min() {
this.getAttribute('price-range-min');
}
set price_range_min(v) {
this.setAttribute('price-range-min', v);
}
get price_range_max() {
this.getAttribute('price-range-max');
}
set price_range_max(v) {
this.setAttribute('price-range-max', v);
}
get state() {
return this.getAttribute('state');
}
set state(v) {
this.setAttribute('state', v);
this.shadowRoot.querySelector('#state').textContent = v;
}
});

View File

@ -1,15 +1,17 @@
import "./shared/rich-text-editor";
import "./shared/form-navigation.js";
import "./shared/image-popup.js";
import "./shared/nav/ow-nav.js";
import "./shared/nav/ow-path.js";
import "./shared/price/price-input";
import "./shared/price/price-view";
import "./ow-account/ow-account.js";
import "./local-businesses/local-businesses.js";
import "./local-businesses/local-business-item";
import "./local-businesses/local-business";
import "./shared/nav/ow-nav.js";
import "./shared/nav/ow-path.js";
import "./shared/price/price-view";
import "./shared/price/price-input";
import "./login-form.js";
import "./register-form.js";
@ -19,9 +21,6 @@ import "./business-items/business-item-editor";
import "./news/ow-articles";
import "./news/news-article";
import "./shared/rich-text-editor";
import "./shared/form-navigation.js";
import "./contacts/contact-info-list";
import "./contacts/contact-info";
import "./contacts/contact-info-editor";
@ -31,6 +30,7 @@ import "./contacts/edit-contact-info";
import "./offers/marketplace-offer";
import "./offers/marketplace-offers";
import "./offers/offer-form";
import "./offers/user-edit-offer";
import "./terms_and_conditions/terms-and-conditions";

View File

@ -12,51 +12,77 @@ customElements.define('marketplace-offer', class extends Component {
<style>
:host {
display: block;
margin-bottom: 8px;
}
img[src=""] { display: none; }
img {
margin-bottom: 16px;
border-bottom: 1px solid var(--border-light-gray-color);
width: 100%;
}
section {
margin-bottom: 16px;
}
#preview {
max-width: 50%;
width: 100%;
}
image-popup {
max-width: 100%;
}
#sep {
display: block;
grid-area: sep;
text-align: center;
}
:host([price-range-max="0"]) #sep {
display: none;
}
@media only screen and (min-device-width: 1200px) {
section {
display: grid;
column-gap: 8px;
grid-template-areas: "img desc desc desc desc"
"img desc desc desc desc"
"img _ _ min max";
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;
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: end;
width: 100px;
text-align: left;
}
}
${ INPUT_STYLE }
</style>
<section>
<div id="preview">
<img alt="" src="" id="picture" />
<image-popup src="" id="picture">
</image-popup>
</div>
<p id="description"></p>
<p id="price-min"></p>
<p id="price-max"></p>
<span id="price-min"></span>
<span id="sep">-</span>
<span id="price-max"></span>
</section>
`);
this.#price_range = new PriceRange(0, 0);
@ -103,7 +129,7 @@ customElements.define('marketplace-offer', class extends Component {
if (v === 'free')
this.#price_range = new PriceRange(v, 0);
else if (v.includes(',')) {
const [min, max, ...r] = v.split(',');
const [min, max, ..._] = v.split(',');
this.#price_range = new PriceRange(parseInt(min), parseInt(max));
} else {
this.#price_range.min = parseInt(v);
@ -125,7 +151,6 @@ customElements.define('marketplace-offer', class extends Component {
}
set price_range_max(v) {
console.info('max', v)
this.#price_range.max = v;
this.#displayPrice();
}
@ -133,6 +158,7 @@ customElements.define('marketplace-offer', class extends Component {
#displayPrice() {
const min = this.shadowRoot.querySelector('#price-min');
const max = this.shadowRoot.querySelector('#price-max');
if (this.#price_range.isFree) {
min.innerHTML = ``;
max.innerHTML = `Za darmo`;

View File

@ -2,25 +2,26 @@ import { Component, FORM_STYLE } from "../shared";
customElements.define('offer-form', class extends Component {
static get observedAttributes() {
return ['offer-id', 'description', 'picture-url']
return ['offer-id', 'description', 'picture-url', 'price-range-min', 'price-range-max'];
}
constructor() {
super(`
<style>
:host { display: block; }
section > form {
display: flex;
justify-content: space-between;
}
#imageSection {
margin-right: 16px;
}
#descriptionSection {
width: calc(100% - 230px);
.tip {
font-size: 10px;
font-style: italic;
color: var(--border-slim-color)
}
@media only screen and (min-device-width: 1200px) {
section > form {
display: flex;
justify-content: space-between;
}
#imageSection {
margin-right: 16px;
}
#priceSection {
width: 300px;
margin-right: 16px;
@ -35,27 +36,34 @@ customElements.define('offer-form', class extends Component {
<section>
<form action="/offers/create" method="post">
<div id="imageSection">
<image-input send-original="true"></image-input>
<image-input send-original="true" width="800" height="800"
></image-input>
<input name="picture_url" id="picture_url" type="hidden" />
</div>
<div id="descriptionSection">
<label for="description">Opis</label>
<input name="description" id="description" type="text" />
<textarea name="description" id="description">
</textarea>
</div>
<div id="priceSection">
<div>
<label>Cena minimalna</label>
<price-input id="priceMinUI" value="0"></price-input>
<input name="price_min" id="priceMin" type="hidden" />
<input name="price_min" id="priceMin" type="hidden" value="0" />
<span class="tip">Jeżeli cena minimalna i maksymalna wynoszą 0 produkt będzie dostępny za darmo</span>
</div>
<div>
<label>Cena maksymalna</label>
<price-input id="priceMaxUI" value="0"></price-input>
<input name="price_max" id="priceMax" type="hidden" />
<input name="price_max" id="priceMax" type="hidden" value="0" />
<span class="tip">Pozostaw 0, żeby cena minimalna była jedyną dostępną ceną</span>
</div>
</div>
<div>
<input id="submit" type="submit" value="Utwórz" />
<div>
<slot name="action"></slot>
</div>
</div>
</form>
</section>
@ -90,6 +98,26 @@ customElements.define('offer-form', class extends Component {
}
}
get price_range_min() {
this.getAttribute('price-range-min');
}
set price_range_min(v) {
this.setAttribute('price-range-min', v);
this.shadowRoot.querySelector('#priceMinUI').value = v;
this.shadowRoot.querySelector('#priceMin').value = v;
}
get price_range_max() {
this.getAttribute('price-range-max');
}
set price_range_max(v) {
this.setAttribute('price-range-max', v);
this.shadowRoot.querySelector('#priceMaxUI').value = v;
this.shadowRoot.querySelector('#priceMax').value = v;
}
get description() {
return this.getAttribute('description');
}

View File

@ -0,0 +1,114 @@
import { Component, BUTTON_STYLE } from "../shared";
const MODES = { 'view': 'view', 'form': 'form' };
customElements.define('user-edit-offer', class extends Component {
static get observedAttributes() {
return ['mode', 'offer-id', 'description', 'picture-url', 'price-range-min', 'price-range-max'];
}
constructor() {
super(`
<style>
:host {
display: block;
}
#view, #form {
display: none;
}
:host([mode='view']) #view { display: block; }
:host([mode='form']) #form { display: block; }
#actions > input {
width: 100%;
}
@media only screen and (min-device-width: 1200px) {
:host([mode='view']) #view { display: flex; }
#actions {
margin-left: 16px;
}
}
${BUTTON_STYLE}
</style>
<section id="view">
<slot></slot>
<div id="actions">
<input type="button" value="Edytuj" id="edit" />
<input type="button" slot="action" id="finish" value="Zakończ" />
</div>
</section>
<section id="form">
<offer-form>
<input type="button" slot="action" id="cancel" value="Anuluj" />
</offer-form>
</section>
`);
this.shadowRoot.querySelector('#edit').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'form';
});
this.shadowRoot.querySelector('#cancel').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.mode = 'view';
});
}
get mode() {
const mode = this.getAttribute('mode');
return MODES[mode] || 'view';
}
set mode(mode) {
this.setAttribute('mode', MODES[mode] || 'view');
}
get offer_id() {
const v = parseInt(this.getAttribute('offer-id'));
return isNaN(v) ? null : v;
}
set offer_id(v) {
v = parseInt(v);
this.setAttribute('offer-id', v);
this.shadowRoot.querySelector('offer-form').offer_id = v;
}
get description() {
return this.getAttribute('description');
}
set description(v) {
this.setAttribute('description', v);
this.shadowRoot.querySelector('offer-form').description = v;
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
this.setAttribute('picture-url', v);
this.shadowRoot.querySelector('offer-form').picture_url = v;
}
get price_range_min() {
this.getAttribute('price-range-min');
}
set price_range_min(v) {
this.setAttribute('price-range-min', v);
this.shadowRoot.querySelector('offer-form').price_range_min = v;
}
get price_range_max() {
this.getAttribute('price-range-max');
}
set price_range_max(v) {
this.setAttribute('price-range-max', v);
this.shadowRoot.querySelector('offer-form').price_range_max = v;
}
});

View File

@ -141,6 +141,9 @@ export class Component extends HTMLElement {
}
}
disconnectedCallback() {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV)
return;
@ -190,8 +193,8 @@ export class PriceRange {
#max;
constructor(min, max) {
this.#min = min || 0;
this.#max = max || 0;
this.min = min || 0;
this.max = max || 0;
}
get isFree() {
@ -211,6 +214,8 @@ export class PriceRange {
}
set min(v) {
v = parseInt(v);
if (isNaN(v)) v = 0;
this.#min = v;
}
@ -219,6 +224,8 @@ export class PriceRange {
}
set max(v) {
v = parseInt(v);
if (isNaN(v)) v = 0;
this.#max = v;
}

View File

@ -66,27 +66,35 @@ customElements.define('image-input', class extends Component {
const image = new Image();
const canvas = document.createElement('canvas');
if (this.send_original) {
image.onload = () => {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
canvas.getContext('2d').drawImage(image, 0, 0);
toFile(canvas);
};
let maxWidth = this.width;
let maxHeight = this.height;
image.src = URL.createObjectURL(input.files[0]);
} else {
image.onload = () => {
const width = image.width > image.height ? 200 : (image.width * 200) / image.height;
const height = image.width > image.height ? (image.width * 200) / image.height : 200;
canvas.width = image.width = width;
canvas.height = image.height = height;
image.onload = () => {
if (this.send_original) {
maxWidth = maxWidth < image.naturalWidth ? maxWidth : image.naturalWidth;
maxHeight = maxHeight < image.naturalHeight ? maxHeight : image.naturalHeight;
}
const width = image.width > image.height
? maxWidth
: (image.width * maxHeight) / image.height;
const height = image.width > image.height
? (image.height * maxWidth) / image.width
: maxHeight;
canvas.width = image.width = width;
canvas.height = image.height = height;
if (this.send_original) {
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
toFile(canvas);
};
} else {
canvas.getContext('2d').drawImage(image, 0, 0);
}
toFile(canvas);
image.width = width > image ? 200 : (width * 200) / height;
image.height = width > image ? (width * 200) / height : 200;
};
image.src = URL.createObjectURL(input.files[0]);
}
image.src = URL.createObjectURL(input.files[0]);
view.innerHTML = '';
view.appendChild(image);
});

View File

@ -0,0 +1,120 @@
import { Component } from "../shared";
customElements.define('image-popup', class extends Component {
#listener;
static get observedAttributes() {
return ['src', 'popup'];
}
constructor() {
super(`
<style>
:host {
display: block;
}
#small {
display: block;
width: 100%;
height: 100%;
}
#large {
display: none;
}
#small > img {
max-width: 100%;
max-height: 100%;
}
img[src=''] {
display: none;
}
:host([popup="true"]) #large {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
#background {
background: rgba(120, 120, 120, .6);
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: -1;
}
#large > #image {
margin: 16px;
height: calc(100% - 32px);
width: calc(100% - 32px);
text-align: center;
}
#large > #image > img {
margin: auto;
max-height: 100%;
max-width: 100%;
}
</style>
<div id="small">
<img alt="" src="" />
</div>
<div id="large">
<div id="background"></div>
<div id="image">
<img src="" alt="">
</div>
</div>
`);
this.shadowRoot.querySelector('#small').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.popup = true;
});
this.shadowRoot.querySelector('#large').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.popup = false;
});
this.#listener = ({ key }) => {
if (key !== 'Escape') return;
if (!this.popup) return;
this.popup = false;
};
}
connectedCallback() {
super.connectedCallback();
window.addEventListener('keydown', this.#listener, true);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('keydown', this.#listener, true);
}
get popup() {
return this.getAttribute('popup') === 'true';
}
set popup(v) {
if (v === true || v === 'true')
this.setAttribute('popup', 'true');
else
this.removeAttribute('popup');
}
get src() {
return this.getAttribute('src');
}
set src(v) {
this.setAttribute('src', v);
this.shadowRoot.querySelector('#small img').src = v;
this.shadowRoot.querySelector('#large img').src = v;
}
});

View File

@ -37,6 +37,16 @@ pub enum OfferState {
Banned,
}
impl OfferState {
pub fn as_str(&self) -> &str {
match self {
OfferState::Pending => "pending",
OfferState::Approved => "approved",
OfferState::Banned => "banned",
}
}
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Account {
pub id: i32,
@ -209,6 +219,7 @@ impl<'l> sqlx::Decode<'l, Postgres> for PriceRange {
}
PgValueFormat::Binary => {
let mut bytes = value.as_bytes()?;
// println!("{bytes:?}");
let _len = take_i32(&mut bytes);
@ -228,23 +239,27 @@ impl<'l> sqlx::Decode<'l, Postgres> for PriceRange {
impl<'l> sqlx::Encode<'l, Postgres> for PriceRange {
fn encode_by_ref(&self, buf: &mut <Postgres as HasArguments<'l>>::ArgumentBuffer) -> IsNull {
let _ = 2i32.encode(buf);
fn write_value(n: &i32, buf: &mut <Postgres as HasArguments<'_>>::ArgumentBuffer) {
let _ = 23.encode(buf);
let _ = 4.encode(buf);
let _ = n.encode(buf);
}
match self {
PriceRange::Free => {
let _ = 0.encode(buf);
let _ = 0.encode(buf);
true.encode(buf)
write_value(&0, buf);
write_value(&0, buf);
}
PriceRange::Fixed { value } => {
let _ = value.encode(buf);
let _ = 0.encode(buf);
false.encode(buf)
write_value(value, buf);
write_value(&0, buf);
}
PriceRange::Range { min, max } => {
let _ = min.encode(buf);
let _ = max.encode(buf);
false.encode(buf)
write_value(min, buf);
write_value(max, buf);
}
}
IsNull::No
}
}

View File

@ -16,6 +16,7 @@ pub enum Page {
AdminNews,
AdminCreateNews,
AdminBusinesses,
AdminOffers,
}
impl Page {
@ -26,7 +27,7 @@ impl Page {
pub fn is_admin(&self) -> bool {
matches!(
self,
Page::AdminNews | Page::AdminCreateNews | Page::AdminBusinesses
Page::AdminNews | Page::AdminCreateNews | Page::AdminBusinesses | Page::AdminOffers
)
}
@ -86,6 +87,14 @@ impl Page {
}
}
pub fn select_admin_offers(&self) -> &str {
if matches!(self, Page::AdminOffers) {
"selected"
} else {
""
}
}
pub fn select_account_offers(&self) -> &str {
if matches!(self, Page::AccountOffers) {
"selected"

79
src/queries/accounts.rs Normal file
View File

@ -0,0 +1,79 @@
use tracing::error;
use crate::model::db;
use crate::queries::{Error, Result, T};
#[tracing::instrument]
pub async fn account_by_id(t: &mut T<'_>, id: String) -> Option<db::Account> {
match sqlx::query_as(
r#"
SELECT id, login, email, pass, facebook_id, account_type
FROM accounts
WHERE id = $1 :: INT
"#,
)
.bind(id)
.fetch_optional(t)
.await
{
Ok(res) => res,
Err(e) => {
error!("{e}");
dbg!(e);
None
}
}
}
#[tracing::instrument]
pub async fn account_by_email(t: &mut T<'_>, email: String) -> Result<db::Account> {
sqlx::query_as(
r#"
SELECT id, login, email, pass, facebook_id, account_type
FROM accounts
WHERE email = $1
"#,
)
.bind(&email)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AccountByEmail { email }
})
}
#[tracing::instrument]
pub async fn create_account(t: &mut T<'_>, input: db::CreateAccountInput) -> Result<db::Account> {
sqlx::query_as(
r#"
INSERT INTO accounts (login, email, pass, facebook_id, account_type)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, login, email, pass, facebook_id, account_type
"#,
)
.bind(&input.login)
.bind(&input.email)
.bind(&input.pass)
.bind(&input.facebook_id)
.bind(input.account_type)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
match e {
sqlx::Error::Database(e) => {
if e.message()
== "duplicate key value violates unique constraint \"accounts_email_key\""
{
Error::AccountTaken { input }
} else {
Error::CreateAccount { input }
}
}
_ => Error::CreateAccount { input },
}
})
}

156
src/queries/businesses.rs Normal file
View File

@ -0,0 +1,156 @@
use tracing::error;
use crate::model::db;
use crate::queries::{Error, Result, T};
#[tracing::instrument]
pub async fn set_business_state(
t: &mut T<'_>,
id: i32,
state: db::LocalBusinessState,
) -> Result<db::LocalBusiness> {
sqlx::query_as(
r#"
UPDATE local_businesses
SET state = $2
WHERE id = $1
RETURNING
id,
owner_id,
name,
description,
state
"#,
)
.bind(id)
.bind(state)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::BusinessItemState { id, state }
})
}
#[tracing::instrument]
pub async fn create_local_business(
t: &mut T<'_>,
name: String,
owner_id: i32,
description: String,
) -> Result<db::LocalBusiness> {
sqlx::query_as(
r#"
INSERT INTO local_businesses (name, owner_id, description)
VALUES ($1, $2, $3)
RETURNING id, owner_id, name, description, state
"#,
)
.bind(&name)
.bind(owner_id)
.bind(&description)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::CreateLocalBusiness {
name,
description,
owner_id,
}
})
}
#[tracing::instrument]
pub async fn all_businesses(t: &mut T<'_>) -> Result<Vec<db::LocalBusiness>> {
sqlx::query_as(
r#"
SELECT id, owner_id, name, description, state
FROM local_businesses
GROUP BY id, state
ORDER BY id DESC
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::VisibleBusinesses
})
}
#[tracing::instrument]
pub async fn visible_businesses(t: &mut T<'_>) -> Result<Vec<db::LocalBusiness>> {
sqlx::query_as(
r#"
SELECT id, owner_id, name, description, state
FROM local_businesses
WHERE state != 'Banned'
GROUP BY id, state
ORDER BY id DESC
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::VisibleBusinesses
})
}
#[tracing::instrument]
pub async fn account_business_by_owner_id(
t: &mut T<'_>,
account_id: i32,
) -> Result<db::LocalBusiness> {
sqlx::query_as(
r#"
SELECT id, owner_id, name, description, state
FROM local_businesses
WHERE state != 'Banned' AND owner_id = $1
GROUP BY id, state
ORDER BY id DESC
"#,
)
.bind(account_id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::OwnedBusiness { account_id }
})
}
#[tracing::instrument]
pub async fn update_business(
t: &mut T<'_>,
input: db::UpdateLocalBusinessInput,
) -> Result<db::LocalBusiness> {
sqlx::query_as(
r#"
UPDATE local_businesses
SET
name = $2,
description = $3
WHERE
id = $1
RETURNING
id, owner_id, name, description, state
"#,
)
.bind(input.id)
.bind(&input.name)
.bind(&input.description)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::UpdateBusiness { input }
})
}

139
src/queries/contacts.rs Normal file
View File

@ -0,0 +1,139 @@
use tracing::error;
use crate::model::db;
use crate::queries::{Error, Result, T};
#[tracing::instrument]
pub async fn all_contacts(t: &mut T<'_>) -> Result<Vec<db::ContactInfo>> {
sqlx::query_as(
r#"
SELECT
id,
owner_id,
contact_type,
content
FROM
contacts
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::AllContacts
})
}
#[tracing::instrument]
pub async fn account_contacts(t: &mut T<'_>, account_id: i32) -> Result<Vec<db::ContactInfo>> {
sqlx::query_as(
r#"
SELECT
id,
owner_id,
contact_type,
content
FROM
contacts
WHERE
owner_id = $1
"#,
)
.bind(account_id)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::AccountContacts { account_id }
})
}
#[tracing::instrument]
pub async fn create_contact(
t: &mut T<'_>,
input: db::CreateContactInput,
) -> Result<Vec<db::ContactInfo>> {
sqlx::query_as(
r#"
INSERT INTO contacts ( owner_id, contact_type, content )
VALUES ($1, $2, $3)
RETURNING
id,
owner_id,
contact_type,
content
"#,
)
.bind(input.owner_id)
.bind(&input.contact_type)
.bind(&input.content)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::CreateContact { input }
})
}
#[tracing::instrument]
pub async fn update_contact(
t: &mut T<'_>,
input: db::UpdateContactInput,
) -> Result<Vec<db::ContactInfo>> {
sqlx::query_as(
r#"
UPDATE contacts
SET
contact_type = $3,
content = $4
WHERE id = $1 AND owner_id = $2
RETURNING
id,
owner_id,
contact_type,
content
"#,
)
.bind(input.id)
.bind(input.owner_id)
.bind(&input.contact_type)
.bind(&input.content)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::UpdateContact { input }
})
}
#[tracing::instrument]
pub async fn delete_contact(
t: &mut T<'_>,
id: i32,
account_id: i32,
) -> Result<Vec<db::ContactInfo>> {
sqlx::query_as(
r#"
DELETE FROM contacts
WHERE id = $1 AND owner_id = $2
RETURNING
id,
owner_id,
contact_type,
content
"#,
)
.bind(id)
.bind(account_id)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::DeleteContact { id }
})
}

View File

@ -0,0 +1,374 @@
use std::cmp::Ordering;
use tracing::error;
use crate::model::db;
use crate::queries::{Error, Result, T};
#[tracing::instrument]
pub async fn visible_business_items(t: &mut T<'_>) -> Result<Vec<db::LocalBusinessItem>> {
sqlx::query_as(
r#"
SELECT
id,
local_business_id,
name,
price,
item_order,
picture_url
FROM local_business_items
ORDER BY item_order ASC
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::VisibleBusinessItems
})
}
#[tracing::instrument]
pub async fn all_business_items(t: &mut T<'_>) -> Result<Vec<db::LocalBusinessItem>> {
sqlx::query_as(
r#"
SELECT
id,
local_business_id,
name,
price,
item_order,
picture_url
FROM local_business_items
ORDER BY item_order ASC
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(&e);
Error::VisibleBusinessItems
})
}
#[tracing::instrument]
pub async fn account_items(t: &mut T<'_>, account_id: i32) -> Vec<db::LocalBusinessItem> {
sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_businesses.owner_id = $1
ORDER BY item_order ASC
"#,
)
.bind(account_id)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AllItems
})
.unwrap_or_default()
}
#[tracing::instrument]
pub async fn item_by_id(
t: &mut T<'_>,
account_id: i32,
item_id: i32,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_business_items.id = $1 AND owner_id = $2
ORDER BY item_order ASC
"#,
)
.bind(item_id)
.bind(account_id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::Item { item_id }
})
}
#[tracing::instrument]
pub async fn move_item(
t: &mut T<'_>,
account_id: i32,
item_id: i32,
item_order: i32,
) -> Result<db::LocalBusinessItem> {
let mut current = item_by_id(t, account_id, item_id).await?;
let all: Vec<db::LocalBusinessItem> = sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_businesses.owner_id = $1
ORDER BY item_order ASC
"#,
)
.bind(account_id)
.fetch_all(&mut *t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::Item { item_id }
})?;
let idx = all
.iter()
.position(|p| p.id == item_id)
.ok_or(Error::Item { item_id })?;
dbg!(idx);
match item_order.cmp(&current.item_order) {
Ordering::Less => {
if let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| {
dbg!(prev_idx);
all.get(prev_idx)
}) {
dbg!(
"Less and found",
current.id,
current.item_order,
prev.id,
prev.item_order,
);
dbg!(update_item_order(&mut *t, current.id, prev.item_order).await?);
dbg!(update_item_order(&mut *t, prev.id, current.item_order).await?);
} else {
dbg!("Less and not found, skipping...");
}
}
Ordering::Equal => {
dbg!("Equal, skipping...");
}
Ordering::Greater => {
if let Some(next) = idx.checked_add(1).and_then(|next_idx| {
dbg!(next_idx);
all.get(next_idx)
}) {
dbg!(
"Greater and found",
current.id,
current.item_order,
next.id,
next.item_order,
);
dbg!(update_item_order(&mut *t, current.id, next.item_order).await?);
dbg!(update_item_order(&mut *t, next.id, current.item_order).await?);
} else {
dbg!("Greater and not found, skipping...");
}
}
};
current.item_order = item_order;
Ok(current)
}
#[tracing::instrument]
async fn update_item_order(
t: &mut T<'_>,
id: i32,
item_order: i32,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
UPDATE local_business_items
SET
item_order = $2
WHERE
id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(id)
.bind(item_order)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::UpdateItemOrder { id, item_order }
})
}
#[tracing::instrument]
pub async fn update_item(
t: &mut T<'_>,
input: db::UpdateLocalBusinessItemInput,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
UPDATE local_business_items
SET
name = $3,
price = $4,
picture_url = $5,
item_order = $6
WHERE
local_business_id = $1 AND
id = $2
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(input.local_business_id)
.bind(input.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.item_order)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::UpdateItem { input }
})
}
#[tracing::instrument]
pub async fn create_item(
t: &mut T<'_>,
input: db::CreateLocalBusinessItemInput,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
INSERT INTO local_business_items (local_business_id, name, price, picture_url, item_order)
VALUES ($1, $2, $3, $4, $5)
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.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.item_order)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::CreateItem { input }
})
}
#[tracing::instrument]
pub async fn set_item_order(
t: &mut T<'_>,
item_id: i32,
idx: i32,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
UPDATE local_business_items
SET
item_order = $2
WHERE
id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(item_id)
.bind(idx as i32 + 1)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::SetOrder { item_id, idx }
})
}
#[tracing::instrument]
pub async fn delete_item(t: &mut T<'_>, item_id: i32) -> Result<Option<db::LocalBusinessItem>> {
sqlx::query_as(
r#"
DELETE FROM local_business_items
WHERE id = $1
RETURNING
id,
local_business_id,
name,
price,
item_order,
picture_url
"#,
)
.bind(item_id)
.fetch_optional(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::DeleteItem { item_id }
})
}

File diff suppressed because it is too large Load Diff

168
src/queries/news.rs Normal file
View File

@ -0,0 +1,168 @@
use tracing::error;
use crate::model::db;
use crate::model::db::NewsArticle;
use crate::queries::{Error, Result, T};
#[tracing::instrument]
pub async fn all_news(t: &mut T<'_>) -> Result<Vec<NewsArticle>> {
sqlx::query_as(
r#"
SELECT
id,
title,
body,
status,
published_at,
created_at
FROM
news
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AllNews
})
}
#[tracing::instrument]
pub async fn news_article_by_id(t: &mut T<'_>, id: i32) -> Result<NewsArticle> {
sqlx::query_as(
r#"
SELECT
id,
title,
body,
status,
published_at,
created_at
FROM
news
WHERE id = $1
"#,
)
.bind(id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::NewsArticleById { id }
})
}
#[tracing::instrument]
pub async fn published_news(t: &mut T<'_>) -> Result<Vec<NewsArticle>> {
sqlx::query_as(
r#"
SELECT
id,
title,
body,
status,
published_at,
created_at
FROM
news
WHERE
status = 'Published'
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::PublishedNews
})
}
#[tracing::instrument]
pub async fn create_news_article(
t: &mut T<'_>,
input: db::CreateNewsArticleInput,
) -> Result<NewsArticle> {
sqlx::query_as(
r#"
INSERT INTO news (title, body, status)
VALUES ($1, $2, $3)
RETURNING
id,
title,
body,
status,
published_at,
created_at
"#,
)
.bind(&input.title)
.bind(&input.body)
.bind(input.status)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::CreateNewsArticle { input }
})
}
#[tracing::instrument]
pub async fn update_news_article(
t: &mut T<'_>,
input: db::UpdateNewsArticleInput,
) -> Result<NewsArticle> {
sqlx::query_as(
r#"
UPDATE news
SET title = $2, body = $3, status = $4
WHERE id = $1
RETURNING
id,
title,
body,
status,
published_at,
created_at
"#,
)
.bind(input.id)
.bind(&input.title)
.bind(&input.body)
.bind(input.status)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::UpdateNewsArticle { input }
})
}
#[tracing::instrument]
pub async fn delete_news_article(t: &mut T<'_>, id: i32) -> Result<Option<NewsArticle>> {
sqlx::query_as(
r#"
DELETE FROM news
WHERE id = $1
RETURNING
id,
title,
body,
status,
published_at,
created_at
"#,
)
.bind(id)
.fetch_optional(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::DeleteNewsArticle { id }
})
}

178
src/queries/offers.rs Normal file
View File

@ -0,0 +1,178 @@
use tracing::error;
use crate::model::db;
use crate::model::db::OfferState;
use crate::queries::{Error, Result, T};
#[tracing::instrument]
pub async fn all_offers(t: &mut T<'_>) -> Result<Vec<db::Offer>> {
sqlx::query_as(
r#"
SELECT
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
FROM offers
ORDER BY id ASC
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AllOffers
})
}
#[tracing::instrument]
pub async fn visible_offers(t: &mut T<'_>) -> Result<Vec<db::Offer>> {
sqlx::query_as(
r#"
SELECT
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
FROM offers
WHERE created_at + '2 weeks' > now() AND state = 'Approved'
ORDER BY id ASC
"#,
)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::VisibleOffers
})
}
#[tracing::instrument]
pub async fn account_offers(t: &mut T<'_>, account_id: i32) -> Result<Vec<db::Offer>> {
sqlx::query_as(
r#"
SELECT
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
FROM offers
WHERE owner_id = $1
ORDER BY id ASC
"#,
)
.bind(account_id)
.fetch_all(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AccountOffers
})
}
#[tracing::instrument]
pub async fn create_offer(t: &mut T<'_>, input: db::CreateOfferInput) -> Result<db::Offer> {
sqlx::query_as(
r#"
INSERT INTO offers (description, picture_url, state, search, owner_id, price_range)
VALUES ($1, $2, $3, to_tsvector('polish', $4), $5, $6)
RETURNING
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
"#,
)
.bind(&input.description)
.bind(&input.picture_url)
.bind(input.state)
.bind(&input.description)
.bind(input.owner_id)
.bind(input.price_range)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::CreateOffer { input }
})
}
#[tracing::instrument]
pub async fn update_offer(t: &mut T<'_>, input: db::UpdateOfferInput) -> Result<db::Offer> {
sqlx::query_as(
r#"
UPDATE offers
SET description = $2,
picture_url = $3,
state = $4,
search = to_tsvector('polish', $5),
price_range = $6
WHERE id = $1
RETURNING
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
"#,
)
.bind(input.id)
.bind(&input.description)
.bind(&input.picture_url)
.bind(input.state)
.bind(&input.description)
.bind(&input.price_range)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::UpdateOffer { input }
})
}
#[tracing::instrument]
pub async fn set_offer_state(t: &mut T<'_>, offer_id: i32, state: OfferState) -> Result<db::Offer> {
sqlx::query_as(
r#"
UPDATE offers
SET state = $2
WHERE id = $1
RETURNING
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
"#,
)
.bind(offer_id)
.bind(state)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::SetOfferState { offer_id, state }
})
}

View File

@ -1,13 +1,9 @@
use actix_web::web::{Data, Form, Path, ServiceConfig};
use actix_web::{get, post, web, HttpResponse};
use askama::*;
use sqlx::PgPool;
mod businesses;
mod news;
mod offers;
use crate::model::view::{Page, SetStateBusinessInput};
use crate::model::{db, view};
use crate::routes::{Identity, JsonResult, Result};
use crate::view::{filters, Helper};
use crate::{authorize, queries};
use actix_web::web;
use actix_web::web::ServiceConfig;
#[macro_export]
macro_rules! require_admin {
@ -27,308 +23,11 @@ macro_rules! require_admin {
}};
}
#[derive(Debug, Default, Template)]
#[template(path = "admin/news.html")]
struct AdminNewsTemplate {
page: view::Page,
error: Option<String>,
account: Option<db::Account>,
news: Vec<db::NewsArticle>,
h: Helper,
}
#[get("")]
async fn admin(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
#[get("")]
async fn admin_news(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let news = queries::all_news(&mut t).await.unwrap_or_default();
t.commit().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminNewsTemplate {
page: Page::AdminNews,
news,
..Default::default()
}
.render()
.unwrap(),
))
}
#[derive(Debug, Default, Template)]
#[template(path = "admin/edit.html")]
struct EditTemplate {
page: Page,
error: Option<String>,
account: Option<db::Account>,
article: db::NewsArticle,
h: Helper,
}
#[get("/{id}")]
async fn edit_news_article(
path: Path<(i32,)>,
db: Data<PgPool>,
id: Identity,
) -> Result<HttpResponse> {
let article_id = path.into_inner().0;
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let article = match queries::news_article_by_id(&mut t, article_id).await {
Ok(article) => article,
Err(e) => {
dbg!(e);
return Ok(HttpResponse::BadRequest().finish());
}
};
t.commit().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
EditTemplate {
page: Page::AdminNews,
article,
..Default::default()
}
.render()
.unwrap(),
))
}
#[post("/create")]
async fn create_news_article(
db: Data<PgPool>,
id: Identity,
form: Form<view::CreateNewsArticleInput>,
) -> Result<HttpResponse> {
let form = form.into_inner();
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
if let Err(e) = queries::create_news_article(
&mut t,
db::CreateNewsArticleInput {
title: form.title,
body: form.body,
status: form.status,
},
)
.await
{
dbg!(e);
t.rollback().await.ok();
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
AdminNewsTemplate {
page: Page::AdminCreateNews,
error: Some("Failed".into()),
..Default::default()
}
.render()
.unwrap(),
));
}
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
#[post("/update")]
async fn update_news_article(
db: Data<PgPool>,
id: Identity,
form: Form<view::UpdateNewsArticleInput>,
) -> Result<HttpResponse> {
let form = form.into_inner();
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
match queries::update_news_article(
&mut t,
db::UpdateNewsArticleInput {
id: form.id,
title: form.title,
body: form.body,
status: form.status,
published_at: if matches!(form.status, db::NewsStatus::Published) {
Some(chrono::Utc::now().naive_utc())
} else {
None
},
},
)
.await
{
Err(e) => {
dbg!(e);
t.rollback().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
Ok(..) => {
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
}
}
#[post("/delete")]
async fn delete_news_article(
db: Data<PgPool>,
id: Identity,
form: Form<view::DeleteNewsArticleInput>,
) -> Result<HttpResponse> {
let form = form.into_inner();
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
match queries::delete_news_article(&mut t, form.id).await {
Err(e) => {
dbg!(e);
t.rollback().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin"))
.finish())
}
Ok(..) => {
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin"))
.finish())
}
}
}
#[post("/upload")]
async fn news_article_upload(
payload: actix_multipart::Multipart,
db: Data<PgPool>,
id: Identity,
) -> JsonResult<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(json pool.begin().await);
let account = require_admin!(json; t, id);
t.commit().await.ok();
crate::routes::uploads::hande_upload(payload, Some(account.id), "news").await
}
#[derive(Debug, Default, Template)]
#[template(path = "admin/businesses.html")]
struct AdminBusinessesTemplate {
page: Page,
error: Option<String>,
account: Option<db::Account>,
businesses: Vec<view::LocalBusiness>,
h: Helper,
}
#[get("")]
async fn admin_businesses(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let (services, mut items, mut contacts) = {
use crate::model::db::{LocalBusiness, LocalBusinessItem};
let services: Vec<LocalBusiness> =
queries::all_businesses(&mut t).await.unwrap_or_default();
let items: Vec<LocalBusinessItem> = queries::all_business_items(&mut t)
.await
.unwrap_or_default();
let contacts = queries::all_contacts(&mut t).await.unwrap_or_default();
(services, items, contacts)
};
let businesses: Vec<_> = services
.into_iter()
.map(|service| view::LocalBusiness::from((service, &mut items, &mut contacts)))
.collect();
t.commit().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminBusinessesTemplate {
page: Page::AdminBusinesses,
businesses,
..Default::default()
}
.render()
.unwrap(),
))
}
#[post("/set-state")]
async fn admin_business_set_state(
db: Data<PgPool>,
id: Identity,
form: Form<SetStateBusinessInput>,
) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let form = form.into_inner();
dbg!(&form);
if let Err(e) = queries::set_business_state(&mut t, form.id, form.state).await {
dbg!(e);
}
if let Err(e) = t.commit().await {
dbg!(e);
}
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/businesses"))
.finish())
}
pub fn configure(config: &mut ServiceConfig) {
config.service(
web::scope("/admin")
.service(
web::scope("/news")
.service(admin_news)
.service(create_news_article)
.service(news_article_upload)
.service(edit_news_article)
.service(update_news_article)
.service(delete_news_article),
)
.service(
web::scope("/businesses")
.service(admin_businesses)
.service(admin_business_set_state),
)
.service(admin),
.configure(news::configure)
.configure(businesses::configure)
.configure(offers::configure),
);
}

View File

@ -0,0 +1,89 @@
use actix_web::web::{Data, Form, ServiceConfig};
use actix_web::{get, post, web, HttpResponse};
use askama::*;
use sqlx_core::postgres::PgPool;
use crate::model::view::{Page, SetStateBusinessInput};
use crate::model::{db, view};
use crate::routes::{Identity, Result};
use crate::view::Helper;
use crate::{authorize, ok_or_internal, queries, require_admin};
#[derive(Debug, Default, Template)]
#[template(path = "admin/businesses.html")]
struct AdminBusinessesTemplate {
page: Page,
error: Option<String>,
account: Option<db::Account>,
businesses: Vec<view::LocalBusiness>,
h: Helper,
}
#[get("")]
async fn admin_businesses(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let (services, mut items, mut contacts) = {
use crate::model::db::{LocalBusiness, LocalBusinessItem};
let services: Vec<LocalBusiness> =
queries::all_businesses(&mut t).await.unwrap_or_default();
let items: Vec<LocalBusinessItem> = queries::all_business_items(&mut t)
.await
.unwrap_or_default();
let contacts = queries::all_contacts(&mut t).await.unwrap_or_default();
(services, items, contacts)
};
let businesses: Vec<_> = services
.into_iter()
.map(|service| view::LocalBusiness::from((service, &mut items, &mut contacts)))
.collect();
t.commit().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminBusinessesTemplate {
page: Page::AdminBusinesses,
businesses,
..Default::default()
}
.render()
.unwrap(),
))
}
#[post("/set-state")]
async fn admin_business_set_state(
db: Data<PgPool>,
id: Identity,
form: Form<SetStateBusinessInput>,
) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let form = form.into_inner();
dbg!(&form);
if let Err(e) = queries::set_business_state(&mut t, form.id, form.state).await {
dbg!(e);
}
if let Err(e) = t.commit().await {
dbg!(e);
}
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/businesses"))
.finish())
}
pub fn configure(config: &mut ServiceConfig) {
config.service(
web::scope("/businesses")
.service(admin_businesses)
.service(admin_business_set_state),
);
}

View File

@ -0,0 +1,238 @@
use actix_web::web::{Data, Form, Path, ServiceConfig};
use actix_web::{get, post, web, HttpResponse};
use askama::*;
use sqlx_core::postgres::PgPool;
use crate::model::view::Page;
use crate::model::{db, view};
use crate::routes::{Identity, JsonResult, Result};
use crate::view::{filters, Helper};
use crate::{authorize, ok_or_internal, queries, require_admin};
#[derive(Debug, Default, Template)]
#[template(path = "admin/news.html")]
struct AdminNewsTemplate {
page: view::Page,
error: Option<String>,
account: Option<db::Account>,
news: Vec<db::NewsArticle>,
h: Helper,
}
#[get("")]
async fn admin(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
#[get("")]
async fn admin_news(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let news = queries::all_news(&mut t).await.unwrap_or_default();
t.commit().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminNewsTemplate {
page: Page::AdminNews,
news,
..Default::default()
}
.render()
.unwrap(),
))
}
#[derive(Debug, Default, Template)]
#[template(path = "admin/edit.html")]
struct EditTemplate {
page: Page,
error: Option<String>,
account: Option<db::Account>,
article: db::NewsArticle,
h: Helper,
}
#[get("/{id}")]
async fn edit_news_article(
path: Path<(i32,)>,
db: Data<PgPool>,
id: Identity,
) -> Result<HttpResponse> {
let article_id = path.into_inner().0;
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
let article = match queries::news_article_by_id(&mut t, article_id).await {
Ok(article) => article,
Err(e) => {
dbg!(e);
return Ok(HttpResponse::BadRequest().finish());
}
};
t.commit().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
EditTemplate {
page: Page::AdminNews,
article,
..Default::default()
}
.render()
.unwrap(),
))
}
#[post("/create")]
async fn create_news_article(
db: Data<PgPool>,
id: Identity,
form: Form<view::CreateNewsArticleInput>,
) -> Result<HttpResponse> {
let form = form.into_inner();
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
if let Err(e) = queries::create_news_article(
&mut t,
db::CreateNewsArticleInput {
title: form.title,
body: form.body,
status: form.status,
},
)
.await
{
dbg!(e);
t.rollback().await.ok();
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
AdminNewsTemplate {
page: Page::AdminCreateNews,
error: Some("Failed".into()),
..Default::default()
}
.render()
.unwrap(),
));
}
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
#[post("/update")]
async fn update_news_article(
db: Data<PgPool>,
id: Identity,
form: Form<view::UpdateNewsArticleInput>,
) -> Result<HttpResponse> {
let form = form.into_inner();
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
match queries::update_news_article(
&mut t,
db::UpdateNewsArticleInput {
id: form.id,
title: form.title,
body: form.body,
status: form.status,
published_at: if matches!(form.status, db::NewsStatus::Published) {
Some(chrono::Utc::now().naive_utc())
} else {
None
},
},
)
.await
{
Err(e) => {
dbg!(e);
t.rollback().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
Ok(..) => {
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/news"))
.finish())
}
}
}
#[post("/delete")]
async fn delete_news_article(
db: Data<PgPool>,
id: Identity,
form: Form<view::DeleteNewsArticleInput>,
) -> Result<HttpResponse> {
let form = form.into_inner();
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id);
match queries::delete_news_article(&mut t, form.id).await {
Err(e) => {
dbg!(e);
t.rollback().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin"))
.finish())
}
Ok(..) => {
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin"))
.finish())
}
}
}
#[post("/upload")]
async fn news_article_upload(
payload: actix_multipart::Multipart,
db: Data<PgPool>,
id: Identity,
) -> JsonResult<HttpResponse> {
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(json pool.begin().await);
let account = require_admin!(json; t, id);
t.commit().await.ok();
crate::routes::uploads::hande_upload(payload, Some(account.id), "news").await
}
pub fn configure(config: &mut ServiceConfig) {
config
.service(
web::scope("/news")
.service(admin_news)
.service(create_news_article)
.service(news_article_upload)
.service(edit_news_article)
.service(update_news_article)
.service(delete_news_article),
)
.service(admin);
}

View File

@ -0,0 +1,131 @@
use actix_web::web::{Data, Form, ServiceConfig};
use actix_web::{get, post, web, HttpResponse};
use askama::*;
use serde::{Deserialize, Serialize};
use sqlx_core::postgres::PgPool;
use crate::model::db;
use crate::model::db::PriceRange;
use crate::model::view::Page;
use crate::routes::{Identity, Result};
use crate::view::Helper;
use crate::{authorize, ok_or_internal, queries, require_admin};
#[derive(Debug, Default, Serialize, Template)]
#[template(path = "admin/offers/index.html")]
struct AdminOffersTemplate {
page: Page,
error: Option<String>,
account: Option<db::Account>,
h: Helper,
offers: Vec<db::Offer>,
}
#[get("")]
async fn admin_offers(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let account = require_admin!(&mut t, id);
let offers = queries::all_offers(&mut t).await.unwrap_or_default();
t.commit().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminOffersTemplate {
account: Some(account),
page: Page::AdminOffers,
offers,
error: None,
h: Default::default(),
}
.render()
.unwrap(),
))
}
#[derive(Deserialize)]
struct AffectedOffer {
id: i32,
}
#[post("/ban")]
async fn admin_ban_offer(
db: Data<PgPool>,
id: Identity,
form: Form<AffectedOffer>,
) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let account = require_admin!(&mut t, id);
match queries::set_offer_state(&mut t, form.id, db::OfferState::Banned).await {
Ok(_) => {
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/offers"))
.finish())
}
Err(e) => {
dbg!(e);
let offers = queries::all_offers(&mut t).await.unwrap_or_default();
t.rollback().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminOffersTemplate {
account: Some(account),
page: Page::AdminOffers,
offers,
error: Some("Nie można zbanować oferty".into()),
h: Default::default(),
}
.render()
.unwrap(),
))
}
}
}
#[post("/approve")]
async fn admin_approve_offer(
db: Data<PgPool>,
id: Identity,
form: Form<AffectedOffer>,
) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let account = require_admin!(&mut t, id);
match queries::set_offer_state(&mut t, form.id, db::OfferState::Approved).await {
Ok(_) => {
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/admin/offers"))
.finish())
}
Err(e) => {
dbg!(e);
let offers = queries::all_offers(&mut t).await.unwrap_or_default();
t.rollback().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminOffersTemplate {
account: Some(account),
page: Page::AdminOffers,
offers,
error: Some("Nie można zbanować oferty".into()),
h: Default::default(),
}
.render()
.unwrap(),
))
}
}
}
pub fn configure(config: &mut ServiceConfig) {
config.service(
web::scope("/offers")
.service(admin_offers)
.service(admin_ban_offer)
.service(admin_approve_offer),
);
}

View File

@ -106,7 +106,7 @@ async fn update_offer(
) -> Result<HttpResponse> {
let pool = db.into_inner();
let mut t = ok_or_internal!(pool.begin().await);
let _account = authorize!(&mut t, id);
let account = authorize!(&mut t, id);
let form = form.into_inner();
dbg!(&form);
@ -125,12 +125,31 @@ async fn update_offer(
)
.await
{
Ok(_) => {}
Err(_) => {}
Ok(_) => {
t.commit().await.ok();
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/account/offers"))
.finish())
}
Err(e) => {
dbg!(e);
let offers = queries::account_offers(&mut t, account.id)
.await
.unwrap_or_default();
t.rollback().await.ok();
Ok(HttpResponse::Ok().content_type("text/html").body(
AccountOffersTemplate {
account: Some(account),
offers,
page: Page::AccountOffers,
error: Some("Problem z utworzeniem wpisu".into()),
..Default::default()
}
.render()
.unwrap(),
))
}
}
t.commit().await.ok();
Ok(HttpResponse::NotImplemented().finish())
}
pub fn configure(config: &mut ServiceConfig) {